Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
61b4b56a46 | ||
|
|
b493e8bc6c | ||
|
|
ad0d2b5012 | ||
|
|
45dc8a439c | ||
|
|
412e77f215 | ||
|
|
2a0bf1ebea | ||
|
|
9ce13da6ed | ||
|
|
0e5731166b | ||
|
|
92edd8ea53 | ||
|
|
931d75beaf | ||
|
|
7667cb30a3 | ||
|
|
015daae064 | ||
|
|
e7510ea5f0 | ||
|
|
582b73cb40 | ||
|
|
04ea184ff6 | ||
|
|
0908b2ca6e | ||
|
|
070febd7bf | ||
|
|
e3782231be | ||
|
|
167a547a65 | ||
|
|
8f62c19104 | ||
|
|
82798dc7fc | ||
|
|
3319cf62d5 | ||
|
|
1626818a98 | ||
|
|
a03d6ebacc | ||
|
|
4ee4a44e16 | ||
|
|
40181f8c0c | ||
|
|
14ff02ebe1 | ||
|
|
55574fe12e | ||
|
|
222ee16d51 | ||
|
|
27ced3ee18 | ||
|
|
cefcf5b409 |
@@ -1735,7 +1735,7 @@ jobs:
|
||||
if: ${{ !cancelled() && vars.ENABLE_GITEE_UPLOAD_FALLBACK == 'true' && needs.release-contract.result == 'success' && needs.release.result == 'success' && needs.publish-channels.result == 'success' }}
|
||||
needs: [release-contract, release, publish-channels]
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 120
|
||||
timeout-minutes: 360
|
||||
permissions:
|
||||
contents: read
|
||||
env:
|
||||
@@ -1752,12 +1752,14 @@ jobs:
|
||||
|
||||
- name: Check out trusted release tooling
|
||||
uses: actions/checkout@v4
|
||||
timeout-minutes: 5
|
||||
with:
|
||||
ref: ${{ github.sha }}
|
||||
path: tmp/trusted-release-tooling
|
||||
persist-credentials: false
|
||||
|
||||
- name: Fetch and verify sealed release tag
|
||||
timeout-minutes: 5
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
run: |
|
||||
@@ -1776,7 +1778,7 @@ jobs:
|
||||
path: dist
|
||||
|
||||
- name: Mirror release to Gitee (China)
|
||||
timeout-minutes: 100
|
||||
timeout-minutes: 318
|
||||
run: ./scripts/release/sync-to-gitee.sh
|
||||
env:
|
||||
VERSION: ${{ needs.release-contract.outputs.release_version }}
|
||||
@@ -2180,13 +2182,14 @@ jobs:
|
||||
needs: dispatch-contract
|
||||
if: ${{ !cancelled() && needs.dispatch-contract.result == 'success' && (needs.dispatch-contract.outputs.mode == 'repair_gitee' || needs.dispatch-contract.outputs.mode == 'repair_oss') && github.ref == format('refs/heads/{0}', github.event.repository.default_branch) && github.repository == 'DingTalk-Real-AI/dingtalk-workspace-cli' }}
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 120
|
||||
timeout-minutes: 360
|
||||
permissions:
|
||||
actions: read
|
||||
contents: read
|
||||
steps:
|
||||
- name: Check out trusted release tooling
|
||||
uses: actions/checkout@v4
|
||||
timeout-minutes: 5
|
||||
with:
|
||||
ref: ${{ github.sha }}
|
||||
path: tooling
|
||||
@@ -2194,6 +2197,7 @@ jobs:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Validate repair version
|
||||
timeout-minutes: 2
|
||||
working-directory: tooling
|
||||
env:
|
||||
VERSION: ${{ inputs.repair_gitee_version || inputs.repair_oss_version }}
|
||||
@@ -2208,6 +2212,7 @@ jobs:
|
||||
- name: Verify immutable release authority
|
||||
id: authority
|
||||
uses: actions/github-script@v7
|
||||
timeout-minutes: 5
|
||||
env:
|
||||
VERSION: ${{ inputs.repair_gitee_version || inputs.repair_oss_version }}
|
||||
with:
|
||||
@@ -2330,12 +2335,14 @@ jobs:
|
||||
|
||||
- name: Check out sealed release source
|
||||
uses: actions/checkout@v4
|
||||
timeout-minutes: 5
|
||||
with:
|
||||
ref: ${{ steps.authority.outputs.commit_sha }}
|
||||
path: release-source
|
||||
persist-credentials: false
|
||||
|
||||
- name: Fetch and verify sealed release tag
|
||||
timeout-minutes: 5
|
||||
working-directory: tooling
|
||||
env:
|
||||
VERSION: ${{ inputs.repair_gitee_version || inputs.repair_oss_version }}
|
||||
@@ -2352,6 +2359,7 @@ jobs:
|
||||
"$VERSION" "$RELEASE_COMMIT" "$RELEASE_TAG_OBJECT"
|
||||
|
||||
- name: Require successful Release workflow delivery
|
||||
timeout-minutes: 5
|
||||
working-directory: tooling
|
||||
env:
|
||||
VERSION: ${{ inputs.repair_gitee_version || inputs.repair_oss_version }}
|
||||
@@ -2368,6 +2376,7 @@ jobs:
|
||||
--channel-repair "$target" "$VERSION" "$RELEASE_COMMIT"
|
||||
|
||||
- name: Download and verify immutable GitHub Release assets
|
||||
timeout-minutes: 10
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
VERSION: ${{ inputs.repair_gitee_version || inputs.repair_oss_version }}
|
||||
@@ -2385,7 +2394,7 @@ jobs:
|
||||
|
||||
- name: Mirror release to Gitee (China)
|
||||
if: ${{ needs.dispatch-contract.outputs.mode == 'repair_gitee' }}
|
||||
timeout-minutes: 100
|
||||
timeout-minutes: 318
|
||||
working-directory: tooling
|
||||
run: |
|
||||
"$GITHUB_WORKSPACE/tooling/scripts/release/sync-to-gitee.sh"
|
||||
|
||||
@@ -1,357 +1,51 @@
|
||||
# Repository Agent Guide
|
||||
|
||||
This file applies to the entire repository. Keep changes scoped, preserve
|
||||
unrelated work, and use `gofmt` for every modified Go file.
|
||||
This file applies to the entire repository. Keep it as a routing page: load
|
||||
the detailed guide for the surface you are changing instead of treating this
|
||||
file as a repository wiki.
|
||||
|
||||
## Build and test
|
||||
## Always
|
||||
|
||||
- Build: `go build ./cmd`
|
||||
- Full test suite: `DWS_PACKAGE_VERSION=0.0.0-test go test ./...`
|
||||
- Generate Schema assets: `go generate ./internal/cli`
|
||||
- Check generated drift: `./scripts/policy/check-generated-drift.sh`
|
||||
- Check the Schema contract: `./scripts/policy/check-schema-catalog.sh`
|
||||
- Preserve unrelated and pre-existing work; inspect `git status` before edits.
|
||||
- Make the smallest coherent change and update its tests and user-facing docs.
|
||||
- Use `gofmt` for every modified Go file.
|
||||
- Treat repository code, tests, scripts, and versioned docs as the source of
|
||||
truth. Do not depend on generated Wiki or CodeWiki content.
|
||||
- Do not hand-edit generated Schema Catalog or Agent metadata. Change their
|
||||
reviewed inputs or generators, then regenerate.
|
||||
|
||||
Generated Schema JSON is committed. Change its source inputs and generators,
|
||||
then regenerate; do not hand-edit generated Catalog or Agent metadata files.
|
||||
`internal/cli/schema_command_registry.json` is different: it is a reviewed
|
||||
`CommandRegistry` source, not a generated snapshot. It is the single reviewed
|
||||
source of stable canonical identity,
|
||||
primary paths, aliases, and navigation. Edit it only when reviewed exposure,
|
||||
identity, primary path, or aliases change; parameter, Skill, and metadata-only
|
||||
changes must not rewrite it mechanically.
|
||||
## Read by task
|
||||
|
||||
## Agent Schema contract
|
||||
| Change surface | Required guide |
|
||||
|---|---|
|
||||
| Any implementation or review | [`CONTRIBUTING.md`](CONTRIBUTING.md) and [`docs/coding-agent-guide.md`](docs/coding-agent-guide.md) |
|
||||
| Writing a task for a coding agent | [`docs/coding-agent-task-template.md`](docs/coding-agent-task-template.md) |
|
||||
| Overall architecture or package layering | [`docs/architecture.md`](docs/architecture.md) |
|
||||
| Product command handler behavior | [`internal/helpers/AGENTS.md`](internal/helpers/AGENTS.md) |
|
||||
| Helpers package/file layout or megafile splits | [`docs/helpers-structure-guide.md`](docs/helpers-structure-guide.md) |
|
||||
| Bundled skill authoring (`skills/`) | [`skills/AGENTS.md`](skills/AGENTS.md) and [`docs/skill-authoring-guide.md`](docs/skill-authoring-guide.md) |
|
||||
| CLI paths, flags, Schema, Agent metadata, or generated Catalog | [`docs/schema-contributor-guide.md`](docs/schema-contributor-guide.md) |
|
||||
| CI, release, packaging, or repository automation | [`docs/automation.md`](docs/automation.md) |
|
||||
| Agent identification headers or host integration | [`docs/agent-code.md`](docs/agent-code.md) |
|
||||
|
||||
The Schema data flow is one way:
|
||||
Read the closest code and tests for the affected package as well. Nested
|
||||
`AGENTS.md` files take precedence for their subtrees.
|
||||
|
||||
```text
|
||||
1. app.NewRootCommand()
|
||||
└─ builds the real Cobra command tree and flags
|
||||
## Common checks
|
||||
|
||||
2. schema_command_registry.json
|
||||
+ schema_hints/metadata/<product>.json tool parameters (+ cli_path)
|
||||
└─ forms EffectiveCommandRegistry
|
||||
└─ binds exactly to real Cobra leaves and aliases
|
||||
|
||||
3. Parameter resolution
|
||||
Cobra flags
|
||||
+ schema_parameter_bindings.json
|
||||
+ metadata tool parameters
|
||||
└─ produces ParameterSpec and constraints
|
||||
|
||||
4. Agent and interface semantics
|
||||
schema_hints/selection/<product>.json (selection prose)
|
||||
+ schema_hints/metadata/<product>.json (safety/interface/runtime_gate)
|
||||
+ pinned MCP metadata
|
||||
└─ resolves Agent metadata by source precedence
|
||||
Markdown is evidence only; it is not concatenated into final prose
|
||||
|
||||
5. One typed hub
|
||||
BoundCommandRegistry
|
||||
+ ParameterSpec
|
||||
+ Agent metadata
|
||||
+ Interface metadata
|
||||
└─ resolves every command exactly once into ToolSpec
|
||||
└─ aggregates SchemaRegistry + SchemaIndex
|
||||
|
||||
6. One-way publication
|
||||
SchemaRegistry
|
||||
└─ internal/cli/schema_catalog.json
|
||||
└─ dws schema list/product/group/leaf/--all
|
||||
```
|
||||
|
||||
Parameter overlays from metadata are merged into `EffectiveCommandRegistry`
|
||||
*before* Cobra binding; after that point there is no second identity source and
|
||||
no identity precedence winner. The binder must reject a missing/non-runnable
|
||||
Cobra path, an alias collision, and any native identity annotation that
|
||||
disagrees with the effective registry. A missing native identity annotation is
|
||||
allowed because annotations are implementation-side assertions, not identity
|
||||
fallbacks.
|
||||
|
||||
The assembler resolves every bound command exactly once into one `ToolSpec`.
|
||||
Build-time gates and the snapshot serializer consume that source-resolved typed
|
||||
registry/index. Runtime projections and delivery gates consume the typed
|
||||
registry/index returned by the production snapshot loader. Neither path may
|
||||
reopen annotations, merge source records, or use a previous Catalog or other
|
||||
generated JSON as a source. `schema_catalog.json` is output-only in the
|
||||
generation graph. The production loader decoding the embedded published
|
||||
snapshot is a delivery boundary, not source resolution; it must never create or
|
||||
repair a Cobra command, flag, registry entry, or later Catalog generation.
|
||||
|
||||
This split is architecturally isomorphic to Lark's typed metadata registry,
|
||||
navigation catalog, and schema renderer. DWS intentionally preserves its
|
||||
existing flat JSON wire contract for compatibility; do not treat architectural
|
||||
alignment as permission to make an unversioned wire-format change.
|
||||
|
||||
The reviewed `CommandRegistry` is the sole source of stable command identity
|
||||
and navigation. The executable Cobra tree remains the source of truth for
|
||||
whether a CLI path exists, is runnable, and which flags it accepts. Schema
|
||||
coverage is bidirectional:
|
||||
|
||||
1. Every final `SchemaRegistry` tool, including its serialized Catalog
|
||||
projection, must resolve to an executable Cobra command.
|
||||
2. Every public runnable Cobra leaf must either resolve to Schema or appear as
|
||||
an exact, reviewed exclusion with a non-empty reason in
|
||||
`internal/cli/schema_command_exclusions.json`.
|
||||
|
||||
Do not use prefix or wildcard exclusions: they can silently hide future
|
||||
commands. Remove an exclusion when its command enters Schema; stale, invalid,
|
||||
or duplicate exclusions must fail generation and CI.
|
||||
|
||||
When adding or changing an Agent-visible command, review all relevant inputs:
|
||||
|
||||
- `internal/cli/schema_command_registry.json` for the reviewed
|
||||
`CommandRegistry`: canonical identity, primary CLI path, aliases, and stable
|
||||
navigation. It is the identity source and is not a generated artifact.
|
||||
- `internal/cli/schema_command_registry.schema.json` is its closed,
|
||||
machine-readable editing contract. Preserve the local `$schema` reference;
|
||||
unknown fields, invalid visibility values, stale paths, and collisions fail
|
||||
Go validation and policy.
|
||||
- `internal/cli/schema_hints/metadata/<product>.json` for safety, interface,
|
||||
`runtime_gate`, and optional parameter overlays (`parameters` / `cli_path`).
|
||||
- `internal/cli/schema_hints/selection/<product>.json` for reviewed Agent
|
||||
selection prose (`agent_summary`, `use_when`, `avoid_when`, `examples`).
|
||||
- `internal/cli/schema_hints/index.json` only maps product IDs to those files.
|
||||
- Native Runtime Schema identity annotations, when present, as consistency
|
||||
assertions against `EffectiveCommandRegistry`. They must agree exactly and
|
||||
must never materialize, infer, or override registry identity.
|
||||
- Flag-to-interface property mappings and required/default semantics.
|
||||
- Generated files under `internal/cli/schema_agent_metadata/` and
|
||||
`internal/cli/schema_catalog.json` after running generation.
|
||||
|
||||
Run the reverse-completeness tests whenever the Cobra tree changes. A command
|
||||
that works through `dws <path>` but cannot be found through the matching
|
||||
`dws schema` lookup is a contract failure unless it has a reviewed exact
|
||||
exclusion.
|
||||
|
||||
Metadata parameter overlays must reference an exact public runnable Cobra leaf
|
||||
and real flags. They may override Schema description, interface-property/type
|
||||
mapping, `required`, and `required_when`; they must not create commands or
|
||||
flags, define an interface, or advertise an unknown RPC. Every authored entry
|
||||
requires `reviewed: true` and a non-empty review reason.
|
||||
|
||||
For Agent-authored metadata or selection edits:
|
||||
|
||||
1. Confirm the exact command and flag names in the current Cobra tree.
|
||||
2. Edit only the owning block (`metadata/` or `selection/`); do not mix fields.
|
||||
3. Add the smallest possible entry; do not copy generated Catalog fields into
|
||||
the input.
|
||||
4. Describe user-visible semantics in `review_reason` and parameter
|
||||
descriptions.
|
||||
5. Run generation, drift, Schema policy, and the focused CLI tests before
|
||||
proposing the change.
|
||||
|
||||
## Agent curation workflow (Schema hints)
|
||||
|
||||
Use this workflow when refreshing Agent selection prose and confirmation
|
||||
alignment. Prefer **agent-authored review** over bulk merge scripts that dump
|
||||
`selection-review.json` or Skill Markdown into Catalog fields.
|
||||
|
||||
Human-authored inputs are split into two blocks:
|
||||
|
||||
| Block | Path | Owns |
|
||||
|---|---|---|
|
||||
| **metadata** | `internal/cli/schema_hints/metadata/<product>.json` | `effect` / `risk` / `confirmation` / `idempotency` / `interface_*` / `runtime_gate` / optional `parameters` |
|
||||
| **selection** | `internal/cli/schema_hints/selection/<product>.json` | `agent_summary` / `use_when` / `avoid_when` / `examples` (+ product routing) |
|
||||
|
||||
`index.json` only maps product IDs to those files. Do not mix selection fields
|
||||
into metadata files or metadata fields into selection files.
|
||||
|
||||
### Goals
|
||||
|
||||
1. **Selection prose** is decision-oriented (Feishu/Lark style): trigger intent,
|
||||
sibling-command routing, and outcome shape — not a restatement of the
|
||||
summary. Delivered Catalog provenance is `reviewed_explicit` from
|
||||
`selection/`.
|
||||
2. **Safety** follows Runtime: `confirmation=user_required` iff the tool's
|
||||
metadata `runtime_gate != none` (for example `confirm_delete`, `typed_yes`,
|
||||
`confirm_dangerous`).
|
||||
3. **Parameter overrides** (former Manual `commands`) live on metadata tools as
|
||||
`parameters` (+ `cli_path`) and are applied into EffectiveCommandRegistry.
|
||||
|
||||
### Authoring
|
||||
|
||||
For every curated tool:
|
||||
|
||||
1. Edit `metadata/<product>.json` for safety/interface/gates/parameters.
|
||||
2. Edit `selection/<product>.json` for selection prose (`reviewed: true`,
|
||||
`review_reason`, `source_refs`).
|
||||
3. Run `make generate-schema`. Do not hand-edit generated
|
||||
`schema_agent_metadata/` or `schema_catalog.json`.
|
||||
|
||||
### Pull live MCP descriptions (personal token)
|
||||
|
||||
Pinned `internal/cli/schema_mcp_metadata.json` is a sanitized baseline. Prefer
|
||||
live Schema from a logged-in personal session:
|
||||
Choose checks from the matrix in `docs/coding-agent-guide.md`; do not claim a
|
||||
check that was not run.
|
||||
|
||||
```bash
|
||||
dws auth status # token_valid should be true
|
||||
dws cache refresh # refresh discovery / tools cache
|
||||
dws schema <mcp-canonical> -f json
|
||||
# or CLI path: dws schema --cli-path "drive copy" -f json
|
||||
make coding-agent-harness
|
||||
make build
|
||||
make format-check
|
||||
make test
|
||||
make policy
|
||||
git diff --check
|
||||
```
|
||||
|
||||
Resolve MCP identity via `interface_ref` when CLI canonical ≠ MCP path
|
||||
(example: CLI `drive.copy_document` → live `doc.copy_document`). On pull
|
||||
failure, fall back to Skill + Cobra Help + pinned MCP, and record evidence
|
||||
(for example `live-dws-schema:<path>#FAILED`). Never print or commit tokens.
|
||||
|
||||
Precedence when sources disagree: **Runtime/Cobra > live MCP > pinned MCP >
|
||||
Skill (evidence only)**.
|
||||
|
||||
### Parallel product agents
|
||||
|
||||
Split work by product groups. Each agent must:
|
||||
|
||||
- Read Skill, Cobra/`--help`, Runtime confirmation sites, and live `dws schema`
|
||||
for its tools.
|
||||
- Hand-write selection + metadata; forbid wholesale JSON merges from review
|
||||
dumps.
|
||||
- Edit only its `metadata/<product>.json` and `selection/<product>.json`.
|
||||
- **Never** `git checkout` unrelated product files to “clean scope”.
|
||||
|
||||
### Regenerate and gates
|
||||
|
||||
```bash
|
||||
make generate-schema
|
||||
./scripts/policy/check-runtime-confirmation-truth.sh
|
||||
go test ./internal/app -run '^TestSheetFinalSchemaConfirmationMatchesRuntimeGuards$' -count=1
|
||||
```
|
||||
|
||||
Example rules (fail generation otherwise):
|
||||
|
||||
- At most two examples per tool; no `--yes` in stored examples.
|
||||
- Examples must match live Cobra argv (path, flags, required groups).
|
||||
- No shell comments in examples.
|
||||
|
||||
After generation, spot-check Catalog: selection provenance is
|
||||
`reviewed_explicit` from `selection/`, and `user_required` count equals
|
||||
metadata `runtime_gate != none`.
|
||||
|
||||
`make generate-schema` is a full deterministic snapshot rebuild, not an
|
||||
incremental patch over the previous Catalog. It rereads every reviewed input,
|
||||
removes stale generated product metadata, and rewrites the exact metadata and
|
||||
Catalog projections. Incremental work happens only when an Agent or human
|
||||
edits selected `metadata/` or `selection/` entries; the next publication still
|
||||
recomputes all outputs. Generated files must never be read back as merge input,
|
||||
and byte guards fail generation if it changes the hint inputs or CommandRegistry.
|
||||
|
||||
Selection prose may choose a more or less restrictive recommendation. It cannot
|
||||
create a Cobra command or flag, change parameter facts, invent an
|
||||
RPC/interface, alter safety metadata, or bypass command completeness. Examples
|
||||
must use an executable primary/alias path and flags accepted by the live Cobra
|
||||
command; never add `--yes` to stored examples.
|
||||
|
||||
Every example is always checked against its real `BoundCommand`: exact path,
|
||||
accepted flags, Cobra required flags/positionals, and the effective
|
||||
`require_one_of`, `require_together`, and `mutually_exclusive` constraints must
|
||||
all pass before execution eligibility is considered. A missing required value,
|
||||
constraint failure, runtime error, or MCP resolution error is a contract bug;
|
||||
none is a valid reason to skip an example.
|
||||
|
||||
Example execution defaults to contract validation only. Runtime execution is
|
||||
opt-in: an example enters `dry_run` only when its final `ToolSpec` publishes an
|
||||
explicit reviewed dry-run capability. The test never injects `--yes`, and
|
||||
`risk`/`confirmation` values do not manufacture preview support. A narrow
|
||||
runtime precondition that cannot be derived from the typed contract may use an
|
||||
exact zero-based `example_dispositions` entry with `mode=contract_only`,
|
||||
`reviewed=true`, one of the schema-enumerated reason codes, and a concrete
|
||||
non-empty reason. Such a disposition may only narrow an explicit dry-run
|
||||
capability; it cannot turn an ordinary contract-only example into a skip.
|
||||
Duplicate, missing, and out-of-range indexes fail validation. Never catch a
|
||||
dry-run failure and dynamically downgrade it to `contract_only`.
|
||||
|
||||
Normal Go tests run the exhaustive contract gate. Run
|
||||
`make test-schema-agent-examples` to additionally execute the eligible subset
|
||||
through the real Cobra `--dry-run` path with isolated HOME and blocked proxies.
|
||||
The test reports stable `total`, `contract`, `dry_run`, `contract_only`,
|
||||
`reviewed_manual`, and per-reason counts; changing those counts requires a
|
||||
review of the corresponding typed dry-run capability or manual disposition.
|
||||
This target is also part of `make policy`.
|
||||
|
||||
Treat every tool `use_when` entry as a reviewed positive selection scenario
|
||||
whose expected result is that tool's canonical path, and every `avoid_when`
|
||||
entry as a reviewed negative scenario that must not choose that tool. The
|
||||
deterministic gate derives a typed evaluation fixture from these same fields;
|
||||
it requires exact tool coverage, a real runnable `BoundCommandRegistry`
|
||||
primary command, at least one positive and negative assertion per tool, and no
|
||||
literal contradictory expectations. It does not claim that string matching
|
||||
proves natural-language understanding.
|
||||
|
||||
Semantic selection is an explicit opt-in live-model check. Run the smoke set
|
||||
(one positive and one negative scenario per product) with
|
||||
`DWS_AGENT_SELECTION_LIVE=1 ARK_API_KEY=... ARK_BASE_URL=... ARK_MODEL=... go test ./internal/app -run TestManualAgentSelectionArkLive -count=1`.
|
||||
Add `DWS_AGENT_SELECTION_FULL=1` to evaluate every committed tool scenario, or
|
||||
set `DWS_AGENT_SELECTION_CASES` to comma-separated fixture case IDs. Normal CI
|
||||
never calls a model; its blockers remain the reproducible fixture, binding,
|
||||
example, provenance, and final-delivery facts.
|
||||
|
||||
The live evaluator sends only case IDs/scenarios plus one same-product
|
||||
candidate table; expected/forbidden assertions stay local and must never be
|
||||
included in the model prompt. Built-in Ark HTTPS bases are allowlisted. A
|
||||
different HTTPS provider requires its exact base in
|
||||
`DWS_AGENT_SELECTION_ALLOWED_BASE_URLS`; plaintext HTTP is accepted only for a
|
||||
loopback test server so API credentials are never sent to an arbitrary clear
|
||||
text endpoint.
|
||||
|
||||
## Safety metadata
|
||||
|
||||
Parameter and safety resolution is mostly source-precedence based and
|
||||
value-neutral: do not choose a winner because one value looks stricter. A
|
||||
higher-priority reviewed metadata/explicit source may intentionally raise or
|
||||
lower description, mapping, `effect`, `risk`, `confirmation`, or `idempotency`.
|
||||
Preserve all candidates and the selected source in provenance, and fail
|
||||
same-precedence conflicts rather than silently merging them.
|
||||
|
||||
`required` is the exception. Cobra `MarkFlagRequired` is a hard floor: the
|
||||
final Agent projection must keep `required=true` and cannot be lowered by
|
||||
manual/hint overlays. Overlays may still raise an optional flag to required.
|
||||
`cli_required` continues to mirror the executable Cobra marker.
|
||||
|
||||
For command text, reviewed `ToolSchemaHint` wins first, then command-specific
|
||||
Cobra Help, then MCP metadata. Generic RPC prose may remain an unselected
|
||||
provenance candidate (and parameter-level `interface_description`); it must not
|
||||
overwrite a specialized leaf's title or description.
|
||||
|
||||
For every delivered `ToolSpec` and `ParameterSpec` field, the provenance
|
||||
winner value must exactly equal the delivered value. Checking only source,
|
||||
count, presence, or hash is not a sufficient final-delivery invariant.
|
||||
|
||||
The same resolved `ToolSpec` must drive every projection. The full leaf payload
|
||||
must equal the corresponding tool in `schema --all` and the full Catalog tool.
|
||||
Overview/product/group summaries and Catalog summaries must equal
|
||||
`ToolSpec.ToSummaryPayload()`. An alias lookup may change only the view fields
|
||||
`cli_path` and `is_alias`; it must not re-resolve or mutate the command
|
||||
contract.
|
||||
|
||||
This build-time rule is distinct from runtime drift handling. If shipped Help
|
||||
and leaf Schema disagree, pass only flags accepted by Cobra. For conflicting
|
||||
safety information, do not silently take the less restrictive behavior: use
|
||||
the safer interpretation or stop and report the contract drift.
|
||||
|
||||
Do not infer one safety field from another. In particular, `effect=destructive`
|
||||
or `risk=high` does not mechanically rewrite `confirmation`; the final
|
||||
precedence winner for each field is authoritative. When
|
||||
`confirmation=user_required`, obtain confirmation before adding `--yes`.
|
||||
Keep CLI confirmation behavior and Schema metadata consistent, and add a
|
||||
semantic regression test through the final embedded loader/query delivery
|
||||
path; a generator unit test or JSON count alone is insufficient.
|
||||
|
||||
## Current Schema boundaries
|
||||
|
||||
- `schema list` remains a progressive overview. `schema --all` is the stable
|
||||
full-export contract: every final `SchemaIndex` tool must contain its
|
||||
complete leaf parameters, constraints, and safety semantics, including an empty
|
||||
`parameters` object for commands without flags. Keep it suitable for the #602
|
||||
compatibility baseline and fail rather than silently emitting a partial
|
||||
export.
|
||||
- `schema --all` is not normal command discovery. Use overview -> product/group
|
||||
-> leaf for routine Agent work. `--compact` is supported for context-saving
|
||||
projections, but a compact full export is not a complete compatibility
|
||||
baseline.
|
||||
- `dws <path> --help` defines whether Cobra exposes a path and which flags the
|
||||
executable accepts. A leaf Schema defines Agent selection, parameter mapping
|
||||
and constraints, and safety/confirmation semantics. A conflict is contract
|
||||
drift, not permission to guess.
|
||||
- Schema and Help describe commands; neither returns DingTalk business data.
|
||||
After discovery, execute the real read/search/list command to obtain data.
|
||||
For Schema work, the minimum generation entry point is `make generate-schema`.
|
||||
For CLI path or flag changes, also run
|
||||
`./scripts/policy/check-command-surface.sh --strict`. Report failures,
|
||||
environment limits, and unrun checks explicitly in the handoff.
|
||||
|
||||
+31
-2
@@ -6,10 +6,39 @@ The format is inspired by [Keep a Changelog](https://keepachangelog.com/) and th
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
## [1.0.54] - 2026-07-21
|
||||
|
||||
This release promotes the validated `v1.0.54-beta.2` baseline to stable. It restores the default transport envelope for personal event output with opt-in flattening, plus Schema CLI path and plugin overlay compatibility fixes.
|
||||
|
||||
### Changed
|
||||
|
||||
- **Personal event output compatibility** (#743) — `event consume` once again preserves the transport envelope by default for `ndjson`/`json`/`pretty`, while retaining the existing `compact` processor. New Agent workflows opt into the event-specific top-level DTO with `--flatten`, which is mutually exclusive with `-f raw` and `--debug-raw-events`; `event schema --flatten` describes that DTO, while the default schema describes `type/event_type/data/headers` and points to `.data | fromjson`.
|
||||
|
||||
### Fixed
|
||||
|
||||
- **Schema CLI path compatibility** — user-facing Schema lookups once again accept space-, dot-, and slash-separated CLI paths without weakening strict canonical identity resolution.
|
||||
- **Plugin CLI overlays** — installed plugins register their manifest-authored command trees again for HTTP and stdio servers, and a plugin may now replace a hidden compatibility fallback (for example `conference`) instead of being skipped as a distribution conflict.
|
||||
- **Schema CLI path compatibility** (#738) — user-facing Schema lookups once again accept space-, dot-, and slash-separated CLI paths without weakening strict canonical identity resolution.
|
||||
- **Plugin CLI overlays** (#701) — installed plugins register their manifest-authored command trees again for HTTP and stdio servers, and a plugin may now replace a hidden compatibility fallback (for example `conference`) instead of being skipped as a distribution conflict.
|
||||
|
||||
## [1.0.54-beta.2] - 2026-07-21
|
||||
|
||||
This beta revalidates the same `v1.0.54-beta.1` source through the cloud release path with a sealed `OSS-Mirror: deferred` policy, because the manually tagged `v1.0.54-beta.1` push run failed on the unavailable OSS mirror channel after GitHub and npm delivery.
|
||||
|
||||
### Changed
|
||||
|
||||
- **Release delivery only** — no source changes since `v1.0.54-beta.1`; see that section for the user-visible changes under validation (#743, #738, #701).
|
||||
|
||||
## [1.0.54-beta.1] - 2026-07-21
|
||||
|
||||
This beta validates the restored default transport envelope for personal event output with opt-in flattening, plus Schema CLI path and plugin overlay compatibility fixes, on top of the validated `v1.0.53-beta.7` baseline.
|
||||
|
||||
### Changed
|
||||
|
||||
- **Personal event output compatibility** (#743) — `event consume` once again preserves the transport envelope by default for `ndjson`/`json`/`pretty`, while retaining the existing `compact` processor. New Agent workflows opt into the event-specific top-level DTO with `--flatten`, which is mutually exclusive with `-f raw` and `--debug-raw-events`; `event schema --flatten` describes that DTO, while the default schema describes `type/event_type/data/headers` and points to `.data | fromjson`.
|
||||
|
||||
### Fixed
|
||||
|
||||
- **Schema CLI path compatibility** (#738) — user-facing Schema lookups once again accept space-, dot-, and slash-separated CLI paths without weakening strict canonical identity resolution.
|
||||
- **Plugin CLI overlays** (#701) — installed plugins register their manifest-authored command trees again for HTTP and stdio servers, and a plugin may now replace a hidden compatibility fallback (for example `conference`) instead of being skipped as a distribution conflict.
|
||||
|
||||
## [1.0.53] - 2026-07-21
|
||||
|
||||
|
||||
+7
-3
@@ -10,9 +10,13 @@ under the project [Apache License 2.0](./LICENSE).
|
||||
## Before You Start
|
||||
|
||||
1. Read `README.md`.
|
||||
2. Read the relevant docs under `docs/`.
|
||||
3. Inspect the code and tests for the area you will change.
|
||||
4. Decide the smallest safe change that satisfies the request.
|
||||
2. Normalize the task and select checks with
|
||||
[`docs/coding-agent-guide.md`](./docs/coding-agent-guide.md).
|
||||
3. Read the relevant docs under `docs/`. CLI/Schema/Agent metadata work must
|
||||
also follow
|
||||
[`docs/schema-contributor-guide.md`](./docs/schema-contributor-guide.md).
|
||||
4. Inspect the code and tests for the area you will change.
|
||||
5. Decide the smallest safe change that satisfies the request.
|
||||
|
||||
Maintainers and automation authors should also read
|
||||
`docs/automation.md` for repo-local release and agent workflow
|
||||
|
||||
@@ -1,33 +1,33 @@
|
||||
class DingtalkWorkspaceCliBeta < Formula
|
||||
desc "Automate DingTalk workspace tasks from the terminal (beta channel)"
|
||||
homepage "https://github.com/DingTalk-Real-AI/dingtalk-workspace-cli"
|
||||
version "1.0.53-beta.4"
|
||||
version "1.0.54-beta.2"
|
||||
license "Apache-2.0"
|
||||
keg_only "it is the beta channel and conflicts with dingtalk-workspace-cli"
|
||||
|
||||
on_macos do
|
||||
if Hardware::CPU.arm?
|
||||
url "https://github.com/DingTalk-Real-AI/dingtalk-workspace-cli/releases/download/v1.0.53-beta.4/dws-darwin-arm64.tar.gz"
|
||||
sha256 "32a442d5b42dfed8512a695a7cef513722db6912f3c3168954cfaefb68e0b075"
|
||||
url "https://github.com/DingTalk-Real-AI/dingtalk-workspace-cli/releases/download/v1.0.54-beta.2/dws-darwin-arm64.tar.gz"
|
||||
sha256 "46b57bed1f6e9f7ba007d8a86a6f5eb280fdeb557fc9bb5946f14f9b1f8f0c9f"
|
||||
else
|
||||
url "https://github.com/DingTalk-Real-AI/dingtalk-workspace-cli/releases/download/v1.0.53-beta.4/dws-darwin-amd64.tar.gz"
|
||||
sha256 "00c694677b9ce2e1a711535740681defe0a5f83d6b72140f305483501628faf8"
|
||||
url "https://github.com/DingTalk-Real-AI/dingtalk-workspace-cli/releases/download/v1.0.54-beta.2/dws-darwin-amd64.tar.gz"
|
||||
sha256 "1b7fd08e64b1c86bbcee217604ffe07e0e8f1b3b5c4de518534386972bcf0f9b"
|
||||
end
|
||||
end
|
||||
|
||||
on_linux do
|
||||
if Hardware::CPU.arm?
|
||||
url "https://github.com/DingTalk-Real-AI/dingtalk-workspace-cli/releases/download/v1.0.53-beta.4/dws-linux-arm64.tar.gz"
|
||||
sha256 "98516620e861e516cf846cf418f1a0ad5c5eb9a8681bd066b1fd29f4847aca6a"
|
||||
url "https://github.com/DingTalk-Real-AI/dingtalk-workspace-cli/releases/download/v1.0.54-beta.2/dws-linux-arm64.tar.gz"
|
||||
sha256 "108d3861ef606519f9934530d29654eab55a73607d1ee6775461f98ef5a6acd4"
|
||||
else
|
||||
url "https://github.com/DingTalk-Real-AI/dingtalk-workspace-cli/releases/download/v1.0.53-beta.4/dws-linux-amd64.tar.gz"
|
||||
sha256 "266df80e8a989971789a157dd134685a7c9eda01dd1d082719ec9e09d5a07bf0"
|
||||
url "https://github.com/DingTalk-Real-AI/dingtalk-workspace-cli/releases/download/v1.0.54-beta.2/dws-linux-amd64.tar.gz"
|
||||
sha256 "6cb96ee09419bbbcc1eb336218ac2aa1d9ca0ed5cbd5a80c79bc20e1e1f03ff7"
|
||||
end
|
||||
end
|
||||
|
||||
resource "skills" do
|
||||
url "https://github.com/DingTalk-Real-AI/dingtalk-workspace-cli/releases/download/v1.0.53-beta.4/dws-skills.zip"
|
||||
sha256 "6770511ab9b04b4d97da1858069ff694830ba91d47c1e8564fd85856f95b016e"
|
||||
url "https://github.com/DingTalk-Real-AI/dingtalk-workspace-cli/releases/download/v1.0.54-beta.2/dws-skills.zip"
|
||||
sha256 "572b93f04a10268d185ad1f8e70e0d412949ae056be8494a9949387076fd14bc"
|
||||
end
|
||||
|
||||
def install
|
||||
|
||||
@@ -1,32 +1,33 @@
|
||||
class DingtalkWorkspaceCli < Formula
|
||||
desc "Automate DingTalk workspace tasks from the terminal"
|
||||
homepage "https://github.com/DingTalk-Real-AI/dingtalk-workspace-cli"
|
||||
version "1.0.52"
|
||||
version "1.0.54"
|
||||
license "Apache-2.0"
|
||||
|
||||
|
||||
on_macos do
|
||||
if Hardware::CPU.arm?
|
||||
url "https://github.com/DingTalk-Real-AI/dingtalk-workspace-cli/releases/download/v1.0.52/dws-darwin-arm64.tar.gz"
|
||||
sha256 "4f6b4d064a76bcefac42feb5f356253fe43f9499b8cec9d2cdf202e7d3b9b60c"
|
||||
url "https://github.com/DingTalk-Real-AI/dingtalk-workspace-cli/releases/download/v1.0.54/dws-darwin-arm64.tar.gz"
|
||||
sha256 "8ae0e52cf973f6fb3df61c67a41fd11e2df417a0c815762b6060cbcb5e600c08"
|
||||
else
|
||||
url "https://github.com/DingTalk-Real-AI/dingtalk-workspace-cli/releases/download/v1.0.52/dws-darwin-amd64.tar.gz"
|
||||
sha256 "abc87128f4b98d0a01ea99235449031971db8fa4ce94167403e3b736c4b81e9a"
|
||||
url "https://github.com/DingTalk-Real-AI/dingtalk-workspace-cli/releases/download/v1.0.54/dws-darwin-amd64.tar.gz"
|
||||
sha256 "11b711b9d70dea62304bf5f8206c56b4e7ea91148dafe97fb7c0f844a2a61da3"
|
||||
end
|
||||
end
|
||||
|
||||
on_linux do
|
||||
if Hardware::CPU.arm?
|
||||
url "https://github.com/DingTalk-Real-AI/dingtalk-workspace-cli/releases/download/v1.0.52/dws-linux-arm64.tar.gz"
|
||||
sha256 "0d357ef0535f99f2f63b5ecbfdee9c32448be2a2c24f3096c03126b3b7570bc5"
|
||||
url "https://github.com/DingTalk-Real-AI/dingtalk-workspace-cli/releases/download/v1.0.54/dws-linux-arm64.tar.gz"
|
||||
sha256 "9c7ecb4c8cd55644b2faa73f6ce7843c0279b23793e23deb5061692ea71a0cf1"
|
||||
else
|
||||
url "https://github.com/DingTalk-Real-AI/dingtalk-workspace-cli/releases/download/v1.0.52/dws-linux-amd64.tar.gz"
|
||||
sha256 "b7dfd9a4b3489211359261747ed0cb9c8c261434bb762ad3f76df33bdbabd5cb"
|
||||
url "https://github.com/DingTalk-Real-AI/dingtalk-workspace-cli/releases/download/v1.0.54/dws-linux-amd64.tar.gz"
|
||||
sha256 "8a0bc245747fc3facf98c8103c06da46852a30bff31ac93b0aa874e8c7e46db7"
|
||||
end
|
||||
end
|
||||
|
||||
resource "skills" do
|
||||
url "https://github.com/DingTalk-Real-AI/dingtalk-workspace-cli/releases/download/v1.0.52/dws-skills.zip"
|
||||
sha256 "0fa3c8dec500c1659e6480d6772ae901b2d12d24322dd5d7283f016024290c21"
|
||||
url "https://github.com/DingTalk-Real-AI/dingtalk-workspace-cli/releases/download/v1.0.54/dws-skills.zip"
|
||||
sha256 "7450fd0115c75bfe6820c7099f348973d9353cca9d8d647c9cddcd70978a7ec0"
|
||||
end
|
||||
|
||||
def install
|
||||
@@ -52,6 +53,7 @@ class DingtalkWorkspaceCli < Formula
|
||||
<<~EOS
|
||||
Agent Skills are bundled in #{pkgshare}/skills/dws.
|
||||
Run `dws skill setup` to install them into your Agent directories.
|
||||
|
||||
EOS
|
||||
end
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ POLICY_GOTMPDIR ?= $(DWS_POLICY_TMPDIR)/go
|
||||
POLICY_ENV = DWS_POLICY_TMPDIR="$(DWS_POLICY_TMPDIR)" GOTMPDIR="$(POLICY_GOTMPDIR)"
|
||||
GO_SOURCE_LIST = git ls-files -z --cached --others --exclude-standard -- '*.go'
|
||||
|
||||
.PHONY: all help build rebuild test test-plan lint format-check fmt policy edition-test interface-integrity authoritative-interface-integrity coverage-gate coverage-gate-platform update-interface-baseline reset-interface-baseline schema-compatibility skill-command-integrity cli-smoke mock-mcp-smoke test-schema-agent-examples generate-schema generate-schema-agent-metadata generate-schema-catalog package release release-pre release-stable changelog-pre changelog-stable publish-homebrew-formula setup-hooks
|
||||
.PHONY: all help build rebuild test test-plan lint format-check fmt policy coding-agent-harness coding-agent-task edition-test interface-integrity authoritative-interface-integrity coverage-gate coverage-gate-platform update-interface-baseline reset-interface-baseline schema-compatibility skill-command-integrity cli-smoke mock-mcp-smoke test-schema-agent-examples generate-schema generate-schema-agent-metadata generate-schema-catalog package release release-pre release-stable changelog-pre changelog-stable publish-homebrew-formula setup-hooks
|
||||
|
||||
all: setup-hooks fmt lint build test rebuild
|
||||
|
||||
@@ -21,6 +21,8 @@ help:
|
||||
@printf " make format-check - Check all repository Go source files with gofmt\n"
|
||||
@printf " make fmt - Format all repository Go source files\n"
|
||||
@printf " make policy - Check the built dws plus open-source and Schema policies\n"
|
||||
@printf " make coding-agent-harness - Validate coding-agent task intake, routing, and self-check contracts\n"
|
||||
@printf " make coding-agent-task TASK=<file> - Validate a filled coding-agent task contract\n"
|
||||
@printf " make interface-integrity - Check historical commands and help contracts still work\n"
|
||||
@printf " make authoritative-interface-integrity BASE_REF=<ref> - Check the Git-owned PR merge-base\n"
|
||||
@printf " make coverage-gate BASE_REF=<ref> - Enforce overall non-regression and 100%% changed-code coverage\n"
|
||||
@@ -86,6 +88,13 @@ policy:
|
||||
@$(POLICY_ENV) ./scripts/policy/check-schema-binary.sh
|
||||
@$(POLICY_ENV) $(MAKE) test-schema-agent-examples
|
||||
|
||||
coding-agent-harness:
|
||||
@./scripts/policy/check-coding-agent-harness.sh
|
||||
|
||||
coding-agent-task:
|
||||
@test -n "$(TASK)" || { printf '%s\n' 'TASK is required, e.g. make coding-agent-task TASK=task.md' >&2; exit 2; }
|
||||
@./scripts/policy/check-coding-agent-harness.sh -task "$(TASK)"
|
||||
|
||||
edition-test:
|
||||
$(GO) test -v -count=1 ./pkg/editiontest/...
|
||||
|
||||
|
||||
@@ -476,6 +476,8 @@ Env vars: `DWS_SKILL_MODE=mono|multi` (also honored by `install.sh` / `install.p
|
||||
|
||||
`dws event consume` subscribes as the currently logged-in user over a managed Stream WebSocket and emits each event as one NDJSON line on stdout. The public catalog currently covers messages that mention the current user, one-to-one messages with a specified user, and messages in a specified group.
|
||||
|
||||
The default `ndjson`, `json`, and `pretty` output preserves the transport envelope (`type`, `event_type`, string `data`, and `headers`) for existing scripts; `compact` retains its existing processor. Add `--flatten` to emit the stable top-level business fields used by Agent workflows. `--format` controls JSON serialization; `--flatten` controls the data structure and cannot be combined with `-f raw` or `--debug-raw-events`.
|
||||
|
||||
> **Prerequisite**: run `dws auth login`. Personal identity is resolved from the OAuth token and cannot be supplied through command-line identity flags.
|
||||
|
||||
For an event-focused installation, use the official convenience installer:
|
||||
@@ -487,19 +489,19 @@ curl -fsSL https://raw.githubusercontent.com/DingTalk-Real-AI/dingtalk-workspace
|
||||
```bash
|
||||
# Inspect the public personal event catalog and schema
|
||||
dws event list
|
||||
dws event schema user_im_message_receive_o2o
|
||||
dws event schema user_im_message_receive_o2o --flatten
|
||||
|
||||
# Listen for messages that mention the current user
|
||||
dws event consume user_im_message_receive_at -f ndjson
|
||||
dws event consume user_im_message_receive_at --flatten -f ndjson
|
||||
|
||||
# Listen for one-to-one messages with a specified user
|
||||
dws event consume user_im_message_receive_o2o --user <userId> -f ndjson
|
||||
dws event consume user_im_message_receive_o2o --user <userId> --flatten -f ndjson
|
||||
|
||||
# Listen by openDingtalkId (external contact, bot, or cross-organization identity)
|
||||
dws event consume user_im_message_receive_o2o --open-dingtalk-id <openDingtalkId> -f ndjson
|
||||
dws event consume user_im_message_receive_o2o --open-dingtalk-id <openDingtalkId> --flatten -f ndjson
|
||||
|
||||
# Listen for messages in a specified group
|
||||
dws event consume user_im_message_receive_group --group <openConversationId> -f ndjson
|
||||
dws event consume user_im_message_receive_group --group <openConversationId> --flatten -f ndjson
|
||||
|
||||
# Inspect local consumers and cancel a subscription
|
||||
dws event status
|
||||
|
||||
+7
-5
@@ -470,6 +470,8 @@ DWS_SKILL_SOURCE=/path/to/skills dws skill setup --mode multi
|
||||
|
||||
`dws event consume` 使用当前 OAuth 登录用户建立托管的 Stream WebSocket 长连接,并把每条事件以 NDJSON 一行输出到 stdout。当前公开目录包括:当前用户被 @ 的消息、与指定用户的单聊消息、指定群的消息。
|
||||
|
||||
默认 `ndjson`、`json`、`pretty` 输出保留兼容 transport envelope(`type`、`event_type`、字符串 `data`、`headers`),`compact` 继续沿用原 processor。Agent 或新脚本显式加 `--flatten` 后,输出稳定的顶层业务字段。`--format` 控制 JSON 序列化,`--flatten` 控制数据结构,且不能与 `-f raw` 或 `--debug-raw-events` 同时使用。
|
||||
|
||||
> **前置条件**:先运行 `dws auth login`。个人身份从 OAuth token 解析,不允许通过命令行伪造。
|
||||
|
||||
只需要 event 能力时,可以使用官方便捷安装脚本:
|
||||
@@ -481,19 +483,19 @@ curl -fsSL https://raw.githubusercontent.com/DingTalk-Real-AI/dingtalk-workspace
|
||||
```bash
|
||||
# 查看公开个人事件目录和 schema
|
||||
dws event list
|
||||
dws event schema user_im_message_receive_o2o
|
||||
dws event schema user_im_message_receive_o2o --flatten
|
||||
|
||||
# 监听当前用户被 @ 的消息
|
||||
dws event consume user_im_message_receive_at -f ndjson
|
||||
dws event consume user_im_message_receive_at --flatten -f ndjson
|
||||
|
||||
# 监听与指定用户的单聊消息
|
||||
dws event consume user_im_message_receive_o2o --user <userId> -f ndjson
|
||||
dws event consume user_im_message_receive_o2o --user <userId> --flatten -f ndjson
|
||||
|
||||
# 使用 openDingtalkId 监听外部联系人、机器人或跨组织身份
|
||||
dws event consume user_im_message_receive_o2o --open-dingtalk-id <openDingtalkId> -f ndjson
|
||||
dws event consume user_im_message_receive_o2o --open-dingtalk-id <openDingtalkId> --flatten -f ndjson
|
||||
|
||||
# 监听指定群的消息
|
||||
dws event consume user_im_message_receive_group --group <openConversationId> -f ndjson
|
||||
dws event consume user_im_message_receive_group --group <openConversationId> --flatten -f ndjson
|
||||
|
||||
# 查看本地 consume,并取消指定订阅
|
||||
dws event status
|
||||
|
||||
@@ -2,6 +2,29 @@
|
||||
|
||||
`dws` is a Go CLI with a versioned, static command surface for DingTalk MCP capabilities. Cobra help serves humans; the embedded Command Catalog serves AI agents.
|
||||
|
||||
## Change Rules
|
||||
|
||||
Prescriptive layering for new code. Keep descriptions here concise; scoped
|
||||
guides own the details.
|
||||
|
||||
1. Dependencies point inward: `cmd` → `internal/app` → `internal/helpers` →
|
||||
shared layers (`executor`, `transport`, `output`, `errors`, `safety`,
|
||||
`cobracmd`). Shared layers never import `helpers` or `app`.
|
||||
2. New product commands go to `internal/helpers` following
|
||||
[`helpers-structure-guide.md`](helpers-structure-guide.md); do not add
|
||||
product logic to `internal/app`, `internal/cli`, or transport.
|
||||
3. New shared behavior joins the existing shared package that owns the
|
||||
contract; do not create a new shared package for a single caller.
|
||||
4. Schema/Agent metadata changes start from reviewed inputs in `internal/cli`
|
||||
per [`schema-contributor-guide.md`](schema-contributor-guide.md); never
|
||||
hand-edit generated Catalog output.
|
||||
5. Bundled skill content under `skills/` follows
|
||||
[`skill-authoring-guide.md`](skill-authoring-guide.md); skills are embedded
|
||||
via `skills/embed.go` and ship with the binary.
|
||||
6. A new top-level package (under `internal/` or `pkg/`) requires a stated
|
||||
boundary reason in its PR and an update to the Repository Structure list
|
||||
below.
|
||||
|
||||
## High-Level Flow
|
||||
|
||||
1. `cmd` is the CLI entrypoint, invoking `internal/app` to build the root Cobra command tree.
|
||||
|
||||
@@ -0,0 +1,112 @@
|
||||
# Coding Agent Workflow
|
||||
|
||||
This is the task intake and self-check contract for coding agents working in
|
||||
this repository. It is intentionally independent of external Wiki systems:
|
||||
the checked-out repository is the execution context and evidence source.
|
||||
|
||||
## 1. Normalize the task input
|
||||
|
||||
Start from the copyable
|
||||
[`coding-agent-task-template.md`](coding-agent-task-template.md). Keep one
|
||||
primary outcome per task. Fill unknown fields from the issue, nearby code,
|
||||
tests, and versioned docs; state any assumption that can affect behavior.
|
||||
|
||||
For a saved, filled template, run `make coding-agent-task TASK=path/to/task.md`.
|
||||
The local checker rejects missing required fields, unsupported task kinds, and
|
||||
unresolved placeholders before implementation begins.
|
||||
|
||||
Do not invent acceptance criteria that expand the requested behavior. Stop for
|
||||
user input only when the unresolved choice would change externally visible
|
||||
behavior, compatibility, destructive scope, credentials, or external state.
|
||||
|
||||
## 2. Establish the baseline
|
||||
|
||||
1. Run `git status --short` and identify pre-existing changes.
|
||||
2. Read the applicable guides linked from the root `AGENTS.md`.
|
||||
3. Locate the implementation and its closest tests with `rg`/`rg --files`.
|
||||
4. Reproduce the bug or capture the current contract before changing it.
|
||||
5. Pick the smallest owning layer; avoid duplicating policy in a caller when a
|
||||
shared typed layer already owns it.
|
||||
|
||||
Never clean, overwrite, stage, or reformat unrelated user changes. If a
|
||||
required file is already modified, inspect the overlap and preserve both
|
||||
intents or stop with the exact conflict.
|
||||
|
||||
## 3. Implement from authoritative inputs
|
||||
|
||||
- Go behavior belongs in the package that owns the contract, with focused
|
||||
tests beside it.
|
||||
- Product handlers under `internal/helpers` follow
|
||||
[`helpers-structure-guide.md`](helpers-structure-guide.md): thin
|
||||
`{product}.go` wiring plus `{product}_{resource}.go` files; do not enlarge
|
||||
megafiles such as `chat.go`.
|
||||
- Public CLI paths and flags must match the live Cobra tree and compatibility
|
||||
policies.
|
||||
- Schema and Agent-facing changes start from reviewed source inputs; generated
|
||||
outputs are publication artifacts.
|
||||
- Documentation describes behavior that exists in the same change.
|
||||
- Secrets, tokens, local identities, and private endpoints must not enter code,
|
||||
fixtures, logs, or handoff output.
|
||||
|
||||
For generated files, run the repository generator and inspect the resulting
|
||||
diff. A large or unrelated generated diff is a signal to stop and find the
|
||||
wrong input or nondeterminism, not something to accept automatically.
|
||||
|
||||
## 4. Select validation by change surface
|
||||
|
||||
Run the narrow check while iterating, then the applicable admission checks
|
||||
before handoff. `make help` is the authoritative target list.
|
||||
|
||||
| Changed surface | Focused check | Admission checks |
|
||||
|---|---|---|
|
||||
| Documentation only | inspect links/examples | `git diff --check` |
|
||||
| Go implementation | `go test ./path/to/package` | `make format-check`, `make test` |
|
||||
| CLI paths or flags | focused command/help tests | `make build`, `./scripts/policy/check-command-surface.sh --strict`, `make interface-integrity` |
|
||||
| Schema registry, hints, or generators | focused generator/app tests | `make generate-schema`, `./scripts/policy/check-generated-drift.sh`, `./scripts/policy/check-schema-catalog.sh`, `make test-schema-agent-examples` |
|
||||
| Skill command examples | inspect referenced `dws` help | `make skill-command-integrity` |
|
||||
| CI or test sharding | run the affected script/test | `make test-plan`, `make lint`, and the CI workflow's pinned actionlint command when workflows change |
|
||||
| Packaging or installers | focused release-script tests | `make package`, `./scripts/release/verify-package-managers.sh` |
|
||||
| Authentication, transport, or OS-specific code | focused tests, including failure paths | `make test`; run relevant platform checks or disclose the unavailable platform |
|
||||
|
||||
`make policy` is the combined policy gate and is appropriate for command,
|
||||
Schema, generated-asset, or broad cross-cutting changes. Platform credentials
|
||||
and live services are not prerequisites for ordinary unit tests; never turn a
|
||||
missing credential into permission to skip deterministic checks.
|
||||
|
||||
The guide contract itself is executable. Run `make coding-agent-harness` after
|
||||
changing `AGENTS.md`, this guide, the Schema contributor guide, helpers
|
||||
structure guide, or their routed commands and paths. This remains a local,
|
||||
opt-in agent aid and is not wired into CI.
|
||||
|
||||
## Design references
|
||||
|
||||
This repository adapts two patterns without copying their product-specific
|
||||
rules:
|
||||
|
||||
- [Lark CLI's contributor guide](https://github.com/larksuite/cli/blob/5efaf65aec59c33899475bb90e6bff1bc3b5b65c/AGENTS.md): one primary goal, machine-consumable errors/output, and validation selected by behavior surface.
|
||||
- [WeCom CLI's root routing guide](https://github.com/WecomTeam/wecom-cli/blob/9eb7898b959861af879495e211e37431fa908f19/AGENTS.md) and [human helper template](https://github.com/WecomTeam/wecom-cli/blob/9eb7898b959861af879495e211e37431fa908f19/src/helpers/HUMANS.md): a thin root guide, scoped implementation guidance, and a copyable request format.
|
||||
|
||||
## 5. Pre-handoff self-check
|
||||
|
||||
Confirm every applicable item:
|
||||
|
||||
- The diff implements the stated goal and no unrelated cleanup.
|
||||
- Pre-existing changes are still present and were not attributed to this task.
|
||||
- New behavior has a regression test; removed behavior has an explicit reason.
|
||||
- Public command paths, flags, output, exit behavior, and compatibility remain
|
||||
intentional.
|
||||
- Destructive or mutating operations retain the required confirmation path.
|
||||
- Generated files came from their reviewed inputs and generation is clean.
|
||||
- Docs and examples use commands accepted by current help/Schema.
|
||||
- Errors preserve actionable context without leaking secrets.
|
||||
- `git diff --check` passes and the final diff has been read.
|
||||
- Every reported check is labeled passed, failed, or not run with a reason.
|
||||
|
||||
Use this compact handoff shape:
|
||||
|
||||
```text
|
||||
Outcome: what is now true
|
||||
Files: intentional files changed
|
||||
Validation: exact commands and results
|
||||
Limits: unrun checks, environment constraints, follow-ups
|
||||
```
|
||||
@@ -0,0 +1,35 @@
|
||||
# Coding Agent Task Template
|
||||
|
||||
Copy this block into an issue or coding-agent request. One task should have one
|
||||
primary outcome; split unrelated outcomes instead of hiding them in acceptance
|
||||
criteria.
|
||||
|
||||
```text
|
||||
Task kind: bug | feature | refactor | docs | policy | release
|
||||
Goal (one primary outcome):
|
||||
Current behavior and evidence:
|
||||
Acceptance criteria:
|
||||
In scope (packages/files/surfaces):
|
||||
Out of scope:
|
||||
Compatibility constraints:
|
||||
Interface impact (commands/flags/output/errors/exit codes/Schema):
|
||||
Safety or data-mutation constraints:
|
||||
Expected validation:
|
||||
Known environment limitations:
|
||||
```
|
||||
|
||||
For a command or remote-interface task, add the smallest concrete invocation
|
||||
and contract evidence available:
|
||||
|
||||
```text
|
||||
CLI path and example argv:
|
||||
Current --help or Schema excerpt:
|
||||
Remote method and request/response shape, if relevant:
|
||||
Expected stdout/stderr and exit behavior:
|
||||
Mutation preview/confirmation behavior:
|
||||
```
|
||||
|
||||
Do not paste credentials, tokens, private endpoints, or production business
|
||||
data. Use redacted fixtures and say which evidence is unavailable. Save the
|
||||
filled block and run `make coding-agent-task TASK=path/to/task.md` to validate
|
||||
it before implementation.
|
||||
@@ -0,0 +1,214 @@
|
||||
# Helpers Package Structure Guide
|
||||
|
||||
This is the coding-structure contract for product commands under
|
||||
`internal/helpers/`. Business teams and coding agents must follow it when
|
||||
adding or moving CLI leaves. Behavioral contracts (stdout, errors, confirmation,
|
||||
Schema) stay in [`internal/helpers/AGENTS.md`](../internal/helpers/AGENTS.md);
|
||||
this document only owns file layout and split rules.
|
||||
|
||||
Reference implementations already in-tree:
|
||||
|
||||
| Pattern | Use as |
|
||||
|---|---|
|
||||
| [`sheet.go`](../internal/helpers/sheet.go) + `sheet_*.go` | Preferred product layout: thin root wiring, resource files |
|
||||
| [`chat_media_upload.go`](../internal/helpers/chat_media_upload.go) | Incremental extract from a megafile without behavior change |
|
||||
| `connect_*.go` | Concern-based split inside the same `helpers` package |
|
||||
|
||||
Anti-pattern to stop growing: single megafiles such as `chat.go` / `aitable.go`
|
||||
(thousands of lines). New work must not enlarge them.
|
||||
|
||||
## 1. Package boundary
|
||||
|
||||
Keep product handlers in the flat package `helpers`:
|
||||
|
||||
```text
|
||||
internal/helpers/ # package helpers (default)
|
||||
register_products.go # public product registration table
|
||||
{product}.go # product root: new{Product}Command()
|
||||
{product}_{resource}.go # one resource / cohesive concern
|
||||
{product}_{resource}_test.go # focused tests beside the file
|
||||
helpers.go / interfaces.go … # cross-product shared machinery
|
||||
```
|
||||
|
||||
Do **not** create `internal/helpers/{product}/` subpackages by default. The flat
|
||||
package exists so leaves can share unexported helpers (`callMCPToolOnServer`,
|
||||
flag validators, confirmation wrappers, transport adapters) without exporting a
|
||||
public API surface. Split into a subpackage only when the concern is a real
|
||||
library boundary with its own tests and almost no need for helpers-private
|
||||
symbols—and get an explicit review for that exception.
|
||||
|
||||
Repository layers outside helpers stay unchanged:
|
||||
|
||||
| Layer | Owns |
|
||||
|---|---|
|
||||
| `internal/app` | Root/static wiring, plugin load |
|
||||
| `internal/helpers` | Product Cobra trees and handler behavior |
|
||||
| `internal/cobracmd` | Shared Cobra construction primitives |
|
||||
| `internal/executor`, `internal/transport` | Invocation and transport |
|
||||
| `internal/output`, `internal/errors`, `internal/safety` | Projection, failures, confirmation |
|
||||
| `internal/cli` | Schema identity / Agent metadata |
|
||||
|
||||
Ordinary product leaves belong in helpers. Do not push product business mapping
|
||||
into app, transport, or Schema generators.
|
||||
|
||||
## 2. File roles inside a product
|
||||
|
||||
### 2.1 Product root — `{product}.go`
|
||||
|
||||
Owns exactly one entry constructor, registered from `register_products.go`:
|
||||
|
||||
```go
|
||||
func newChatCommand() *cobra.Command { /* wire subgroups only */ }
|
||||
```
|
||||
|
||||
The root file should:
|
||||
|
||||
1. Define the product `cobra.Command` (`Use` / `Short` / `Long` / aliases).
|
||||
2. Call resource factories and `AddCommand` them.
|
||||
3. Register product-level aliases or hint stubs when needed.
|
||||
4. Avoid large inline `RunE` bodies for leaves.
|
||||
|
||||
Target size: wiring-only, roughly under **400 lines** (see `sheet.go` ≈ 270).
|
||||
If the root grows past that because leaves are inlined, extract a resource file.
|
||||
|
||||
### 2.2 Resource / concern file — `{product}_{resource}.go`
|
||||
|
||||
Split on the **CLI path segment or cohesive concern**, not on “one function per
|
||||
file”:
|
||||
|
||||
| CLI path | File |
|
||||
|---|---|
|
||||
| `dws chat group …` | `chat_group.go` |
|
||||
| `dws chat message …` | `chat_message.go` |
|
||||
| `dws chat media …` | `chat_media_upload.go` (or `chat_media.go`) |
|
||||
| `dws sheet filter-view …` | `sheet_filter_view.go` |
|
||||
| `dws sheet dimension …` | `sheet_dimension.go` |
|
||||
|
||||
Each file exposes one or more unexported factories, for example:
|
||||
|
||||
```go
|
||||
func newChatGroupCmd() *cobra.Command { … }
|
||||
func newChatMessageCmd() *cobra.Command { … }
|
||||
func newWorkbookCmds() []*cobra.Command { … }
|
||||
```
|
||||
|
||||
The product root only wires those factories. Prefer keeping a leaf’s flags,
|
||||
`RunE`, and nearby request-mapping helpers in the same resource file until a
|
||||
helper is reused by multiple resources.
|
||||
|
||||
Soft size guide per resource file:
|
||||
|
||||
| Lines | Action |
|
||||
|---|---|
|
||||
| < 600 | Normal |
|
||||
| 600–1000 | Prefer splitting the next cohesive subgroup before adding more |
|
||||
| > 1000 | Required split before landing substantial new leaves |
|
||||
|
||||
“One command per file” is **not** required. Tiny sibling leaves that share
|
||||
flags and mapping belong together. Split when the file holds multiple unrelated
|
||||
resources or becomes hard to review.
|
||||
|
||||
### 2.3 Shared product helpers — `{product}_{concern}.go`
|
||||
|
||||
Use a concern suffix when code is shared across resources and is not itself a
|
||||
command tree:
|
||||
|
||||
- `sheet_validate.go` — shared validation
|
||||
- `chat_args.go` / similar — grant/arg builders used by several leaves
|
||||
- `connect_command.go` — slash-command parsing used by the daemon
|
||||
|
||||
Do not park unrelated products’ utilities in these files. Cross-product
|
||||
machinery belongs in non-prefixed shared files (`helpers.go`, `interfaces.go`,
|
||||
output/error helpers), never copied per product.
|
||||
|
||||
### 2.4 Tests
|
||||
|
||||
- Name tests after the file or scenario: `chat_message_search_test.go`,
|
||||
`sheet_filter_view_test.go`.
|
||||
- Prefer exercising public product construction (`newChatCommand().Find(…)`)
|
||||
for command-surface regressions.
|
||||
- Pure helpers may be unit-tested directly in the same package.
|
||||
- A mechanical file split without behavior change should keep existing tests
|
||||
green with no assertion edits; if tests must change, the split leaked a
|
||||
behavior or visibility change and needs review.
|
||||
|
||||
## 3. Registration and naming
|
||||
|
||||
1. Public products enter the CLI only through the table in
|
||||
`register_products.go` (`new{Product}Command` factories). Do not invent a
|
||||
second registration path for ordinary product leaves.
|
||||
2. Constructor names stay unexported (`new…`) unless a deliberate test helper
|
||||
requires otherwise.
|
||||
3. File names are lowercase snake_case. Match the CLI resource token when
|
||||
practical (`filter-view` → `filter_view`).
|
||||
4. Do not hand-edit generated sync markers in `register_products.go` outside
|
||||
the product-registration workflow that owns that file.
|
||||
|
||||
## 4. How to add a new command (business-team checklist)
|
||||
|
||||
1. Confirm the owning product and CLI path with `dws <product> --help`.
|
||||
2. Open `{product}.go` only to wire `AddCommand`; put the leaf in
|
||||
`{product}_{resource}.go`.
|
||||
3. If the product is still a megafile (`chat.go`, `aitable.go`, …) and your
|
||||
resource file does not exist yet, **create the resource file and move only
|
||||
the subgroup you need** (or add the new leaf there and wire it from the
|
||||
root). Do not append another large leaf into the megafile.
|
||||
4. Keep stdout / stderr / error / confirmation contracts from
|
||||
`internal/helpers/AGENTS.md`.
|
||||
5. If path, flags, safety, or Agent selection change, follow
|
||||
[`schema-contributor-guide.md`](schema-contributor-guide.md).
|
||||
6. Add or extend the closest test; run
|
||||
`go test ./internal/helpers -count=1` (or a tighter `-run`) plus the checks
|
||||
selected by [`coding-agent-guide.md`](coding-agent-guide.md).
|
||||
|
||||
## 5. Migrating an existing megafile
|
||||
|
||||
Mechanical splits are welcome and preferred over “big-bang” rewrites.
|
||||
|
||||
Rules for a split PR:
|
||||
|
||||
1. **Behavior-neutral**: same command paths, flags, help text, request mapping,
|
||||
confirmation, and output.
|
||||
2. **Stable entrypoint**: keep `new{Product}Command()` as the registration
|
||||
symbol; only its body becomes wiring.
|
||||
3. **Move by resource**: extract one CLI subgroup per commit/PR when possible
|
||||
(`group`, `message`, `category`, …).
|
||||
4. **No drive-by cleanups** in the same PR (renames, flag redesign, Schema
|
||||
edits) unless the task explicitly includes them.
|
||||
5. **Stop growing the megafile**: after the first extract, new leaves for that
|
||||
resource go to the new file only.
|
||||
|
||||
Suggested first-wave split for `chat` (illustrative, not mandatory order):
|
||||
|
||||
| Extract to | Contents |
|
||||
|---|---|
|
||||
| `chat.go` | `newChatCommand()` wiring only |
|
||||
| `chat_permission.go` | `chmod`, `data-auth` |
|
||||
| `chat_group.go` | `group` tree |
|
||||
| `chat_message.go` | `message` tree |
|
||||
| `chat_category.go` | `category` tree |
|
||||
| `chat_bot.go` | `bot` tree |
|
||||
| `chat_conversation.go` | top-level conversation ops (`set-top`, mute, red-point, …) |
|
||||
| existing `chat_media_upload.go` | keep; optionally rename to `chat_media.go` only in a dedicated rename PR |
|
||||
|
||||
Apply the same pattern to `aitable`, `attendance`, `mail`, and `doc` when those
|
||||
products take new work.
|
||||
|
||||
## 6. What this guide does not change
|
||||
|
||||
- Wire format, MCP method names, or Schema identity rules.
|
||||
- The choice to keep `package helpers` flat.
|
||||
- Runtime confirmation / dry-run policy (still owned by safety + Schema metadata).
|
||||
- Permission to skip tests because a change was “only a move”—moves still need
|
||||
the product’s focused tests green.
|
||||
|
||||
## 7. Harness check
|
||||
|
||||
After editing this guide or its links from `AGENTS.md` /
|
||||
`internal/helpers/AGENTS.md`, run:
|
||||
|
||||
```bash
|
||||
make coding-agent-harness
|
||||
```
|
||||
|
||||
This check is a local, opt-in agent aid; it is not part of `make policy` or CI.
|
||||
@@ -0,0 +1,150 @@
|
||||
# Schema and Agent Contract Contributor Guide
|
||||
|
||||
Read this guide when changing public CLI commands, Schema identity or
|
||||
parameters, Agent selection/safety metadata, or generated Schema assets.
|
||||
|
||||
## Ownership and data flow
|
||||
|
||||
The publication graph is one way:
|
||||
|
||||
```text
|
||||
Cobra command tree
|
||||
+ reviewed CommandRegistry identity/navigation
|
||||
+ reviewed metadata parameter overlays
|
||||
-> EffectiveCommandRegistry and executable binding
|
||||
+ parameter bindings
|
||||
+ reviewed selection and safety/interface metadata
|
||||
+ pinned MCP metadata
|
||||
-> one resolved ToolSpec registry/index
|
||||
-> generated Agent metadata and embedded Schema Catalog
|
||||
-> dws schema projections and runtime metadata lookup
|
||||
```
|
||||
|
||||
The owning sources are:
|
||||
|
||||
| Concern | Authoritative input |
|
||||
|---|---|
|
||||
| Executable paths and accepted flags | Cobra tree built by `app.NewRootCommand()` |
|
||||
| Stable canonical identity, primary path, aliases, navigation | `internal/cli/schema_command_registry.json` |
|
||||
| Registry editing contract | `internal/cli/schema_command_registry.schema.json` |
|
||||
| Safety, interface, runtime gates, parameter overlays | `internal/cli/schema_hints/metadata/<product>.json` |
|
||||
| Agent selection prose and examples | `internal/cli/schema_hints/selection/<product>.json` |
|
||||
| Product-to-hint-file routing | `internal/cli/schema_hints/index.json` |
|
||||
| Flag/property bindings | `internal/cli/schema_parameter_bindings.json` |
|
||||
| Sanitized interface fallback | `internal/cli/schema_mcp_metadata.json` |
|
||||
| Exact reviewed omissions from Schema | `internal/cli/schema_command_exclusions.json` |
|
||||
|
||||
Generated files under `internal/cli/schema_agent_metadata/` and
|
||||
`internal/cli/schema_catalog.json` are output only. Runtime loading is a delivery
|
||||
boundary: it must not create or repair commands, flags, registry entries, or
|
||||
generation inputs.
|
||||
|
||||
## Invariants
|
||||
|
||||
1. Every delivered tool resolves to a public runnable Cobra leaf.
|
||||
2. Every public runnable Cobra leaf resolves to Schema or has one exact,
|
||||
reviewed exclusion with a non-empty reason. Wildcard/prefix exclusions are
|
||||
forbidden.
|
||||
3. The reviewed CommandRegistry is the only stable identity/navigation source.
|
||||
Native annotations, when present, are consistency assertions and must agree.
|
||||
4. Metadata overlays may describe or constrain real flags; they cannot create
|
||||
commands, flags, interfaces, or unknown RPCs.
|
||||
5. Cobra-required flags are a hard floor. An overlay may make an optional flag
|
||||
required but cannot make a Cobra-required flag optional.
|
||||
6. Each tool is resolved once into one typed `ToolSpec`; all Catalog, `schema
|
||||
--all`, leaf, summary, safety, and runtime projections derive from it.
|
||||
7. Provenance winner values must equal delivered values. Same-precedence
|
||||
conflicts fail instead of being merged silently.
|
||||
8. `confirmation=user_required` requires user confirmation before `--yes`.
|
||||
Do not infer confirmation mechanically from risk/effect; keep runtime gates
|
||||
and published metadata consistent.
|
||||
9. Stored examples use real primary/alias paths and accepted flags, satisfy all
|
||||
required/constraint rules, contain no shell comments, and never add `--yes`.
|
||||
10. `schema --all` remains the complete compatibility export. Routine discovery
|
||||
should use overview, product/group, then leaf queries.
|
||||
|
||||
When Help and shipped Schema disagree, treat it as contract drift. Cobra still
|
||||
defines executable flags; use the safer interpretation for confirmation or
|
||||
stop rather than guessing.
|
||||
|
||||
Parameter and safety resolution is source-precedence based and otherwise
|
||||
value-neutral: a value must not win merely because it looks stricter. Preserve
|
||||
all candidates and the selected source, and fail same-precedence conflicts.
|
||||
Command text resolves from reviewed tool hints, then command-specific Cobra
|
||||
help, then MCP metadata; generic RPC prose must not replace a specialized
|
||||
leaf's description. An alias lookup may change only view fields such as
|
||||
`cli_path` and `is_alias`, never the resolved command contract.
|
||||
|
||||
## Editing workflow
|
||||
|
||||
1. Confirm the live path and flags in the Cobra tree and current `--help`.
|
||||
2. Change only the owning reviewed block. Do not copy generated Catalog fields
|
||||
into inputs or mix selection fields into metadata files.
|
||||
3. Keep registry edits limited to intentional identity/navigation changes.
|
||||
4. For selection prose, write decision-oriented routing: when to choose the
|
||||
command, when a sibling is better, and the result shape. Do not restate help.
|
||||
5. For parameter overlays, use an exact runnable leaf and real flags; set
|
||||
`reviewed: true` with a concrete review reason.
|
||||
6. Regenerate the complete snapshot; publication is deterministic even when
|
||||
only one product input changed.
|
||||
7. Inspect authored and generated diffs separately, then run the gates below.
|
||||
|
||||
Pinned MCP metadata is a sanitized fallback. When a task requires refreshing
|
||||
it and a personal session is available, inspect live metadata with `dws auth
|
||||
status`, `dws cache refresh`, and `dws schema <canonical> -f json`. Never print
|
||||
or commit tokens. Evidence precedence is Runtime/Cobra, live MCP, pinned MCP,
|
||||
then Skill prose as evidence only.
|
||||
|
||||
## Required checks
|
||||
|
||||
```bash
|
||||
make generate-schema
|
||||
./scripts/policy/check-runtime-confirmation-truth.sh
|
||||
./scripts/policy/check-generated-drift.sh
|
||||
./scripts/policy/check-schema-catalog.sh
|
||||
./scripts/policy/check-command-surface.sh --strict
|
||||
make test-schema-agent-examples
|
||||
```
|
||||
|
||||
Also run focused tests for the changed binder, generator, command, or runtime
|
||||
consumer. Run reverse-completeness tests whenever the Cobra tree changes.
|
||||
|
||||
Agent examples are contract-checked by default. Eligible reviewed dry-run
|
||||
examples are additionally exercised by `make test-schema-agent-examples` with
|
||||
isolated state; a runtime failure must not be converted into an ad hoc skip.
|
||||
Live-model selection evaluation is optional and never a normal CI dependency.
|
||||
|
||||
An example enters runtime dry-run only when its final typed contract publishes
|
||||
an explicit reviewed dry-run capability. Risk or confirmation metadata does
|
||||
not manufacture preview support, and the harness never injects `--yes`. A
|
||||
narrow precondition that cannot be derived from the contract may use an exact,
|
||||
reviewed `example_dispositions` entry to narrow dry-run to contract-only; it
|
||||
must not become a general skip or a fallback applied after execution fails.
|
||||
|
||||
Every `use_when` entry is a positive selection fixture and every `avoid_when`
|
||||
entry is a negative fixture for that tool. The deterministic gate checks
|
||||
coverage and contradictions; it does not claim to prove natural-language
|
||||
understanding. When explicitly requested, the optional live-model smoke test
|
||||
can be run with:
|
||||
|
||||
```bash
|
||||
DWS_AGENT_SELECTION_LIVE=1 \
|
||||
ARK_API_KEY=... ARK_BASE_URL=... ARK_MODEL=... \
|
||||
go test ./internal/app -run TestManualAgentSelectionArkLive -count=1
|
||||
```
|
||||
|
||||
Use `DWS_AGENT_SELECTION_FULL=1` for the full fixture or
|
||||
`DWS_AGENT_SELECTION_CASES=<comma-separated-ids>` for selected cases. A custom
|
||||
HTTPS provider must be explicitly allowlisted; plaintext is accepted only for
|
||||
a loopback test server so credentials are not sent over arbitrary clear text.
|
||||
|
||||
## Runtime boundaries
|
||||
|
||||
- `schema list` is a progressive overview; `schema --all` is the complete,
|
||||
non-compact compatibility baseline with full parameters, constraints, and
|
||||
safety semantics.
|
||||
- `--compact` saves discovery context but is not a full compatibility export.
|
||||
- `dws <path> --help` decides whether a path and its flags are executable. Leaf
|
||||
Schema owns Agent selection, mapping, constraints, and safety semantics.
|
||||
- Help and Schema describe commands; they do not return DingTalk business
|
||||
data. Execute the real read/list/search command after discovery.
|
||||
@@ -0,0 +1,75 @@
|
||||
# Skill Authoring Guide
|
||||
|
||||
Contract for the bundled agent skills under `skills/`. Skills are embedded via
|
||||
`skills/embed.go` and installed by `dws skill setup`, so every edit ships with
|
||||
the binary. Keep skill prose concise: long command references belong in
|
||||
`references/`, not in `SKILL.md`.
|
||||
|
||||
## Layout
|
||||
|
||||
| Path | Role |
|
||||
|---|---|
|
||||
| `skills/mono/` | Single bundled skill (stable mode) with shared `references/` and `scripts/` |
|
||||
| `skills/multi/dingtalk-<product>/` | One skill per product (experimental mode) |
|
||||
| `skills/multi/dws-shared/` | Shared prerequisite: auth, global flags, routing, safety |
|
||||
| `skills/embed.go` | Embeds `mono` + `multi`; do not add new roots |
|
||||
|
||||
A product skill directory contains:
|
||||
|
||||
```text
|
||||
dingtalk-<product>/
|
||||
SKILL.md # frontmatter + concise routing/usage prose
|
||||
references/ # long command references, playbooks
|
||||
scripts/ # executable recipes (python), kept minimal
|
||||
```
|
||||
|
||||
## SKILL.md contract
|
||||
|
||||
Frontmatter:
|
||||
|
||||
```yaml
|
||||
---
|
||||
name: dingtalk-<product>
|
||||
description: <触发场景>. Use when … Distinct from <相邻skill>(…). 命令前缀:dws <product>。
|
||||
cli_version: ">=<minimum dws version>"
|
||||
metadata:
|
||||
category: product
|
||||
stability: experimental
|
||||
requires:
|
||||
bins:
|
||||
- dws
|
||||
---
|
||||
```
|
||||
|
||||
Body rules:
|
||||
|
||||
- State the safety rules directly in concise prose; there is no injected
|
||||
preamble mechanism in this repository.
|
||||
- State the `dws-shared` prerequisite for multi skills.
|
||||
- Route by intent: shortcuts table first when one covers the scenario, then
|
||||
scripts/recipes, then atomic commands with `dws schema` / `--help`.
|
||||
- Every referenced `dws` command must exist in the current binary; verify with
|
||||
`dws <cmd> --help` and keep prose version-agnostic ("以当前 dws 二进制为准").
|
||||
- Mutating commands must point at the leaf Schema `confirmation` contract; do
|
||||
not invent confirmation rules in prose.
|
||||
|
||||
## Dual-write rule
|
||||
|
||||
Skill behavior described in prose must match the CLI it references. When a
|
||||
command, flag, or confirmation contract changes, update the affected `SKILL.md`
|
||||
/ `references/` in the same change; when skill routing changes, check whether
|
||||
Schema selection hints (`internal/cli/schema_hints/`) need a reviewed update
|
||||
per [`schema-contributor-guide.md`](schema-contributor-guide.md).
|
||||
|
||||
## Validation
|
||||
|
||||
| Change | Check |
|
||||
|---|---|
|
||||
| Any skill edit | `make skill-command-integrity` |
|
||||
| Referenced CLI surface changed | `./scripts/policy/check-command-surface.sh --strict` |
|
||||
| Schema hints touched | `make generate-schema` + schema gates |
|
||||
| Recipe scripts | run the script's own smoke path or `test/skill_e2e` when applicable |
|
||||
|
||||
`make skill-command-integrity` builds `scripts/policy/skill-command-check` and
|
||||
verifies every `dws` command referenced by skills resolves against the current
|
||||
binary. Run it before handoff; do not claim a command exists without it.
|
||||
@@ -1469,10 +1469,10 @@ func TestCrossPlatformCoveragePersonalEventPureCoverage(t *testing.T) {
|
||||
if !ok {
|
||||
t.Fatal("mention definition missing")
|
||||
}
|
||||
if err := renderPersonalSchema(io.Discard, def, ""); err != nil {
|
||||
if err := renderPersonalSchema(io.Discard, def, "", false); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := renderPersonalSchema(io.Discard, def, "yaml"); err == nil {
|
||||
if err := renderPersonalSchema(io.Discard, def, "yaml", true); err == nil {
|
||||
t.Fatal("unsupported schema format succeeded")
|
||||
}
|
||||
for _, key := range []string{"", "unknown", personal.EventMention, personal.EventFromUser} {
|
||||
|
||||
@@ -112,6 +112,7 @@ func newEventConsumeCommand() *cobra.Command {
|
||||
dryRun bool
|
||||
foreground bool
|
||||
asIdentity string
|
||||
flatten bool
|
||||
personalOpts personalConsumeOptions
|
||||
streamOpts eventStreamTicketOptions
|
||||
)
|
||||
@@ -127,7 +128,11 @@ func newEventConsumeCommand() *cobra.Command {
|
||||
json 每事件多行美化 JSON(必须配 --max-events 或 --duration)
|
||||
pretty 同 json,未来加颜色
|
||||
raw 仅 SDK 原始 payload,无外层封装
|
||||
compact 扁平化 + 解析嵌套 + 抽取语义字段(Agent 友好)
|
||||
compact 单行紧凑 JSON;不传 --flatten 时沿用原 compact processor
|
||||
|
||||
数据结构:
|
||||
ndjson/json/pretty 默认保持 transport envelope(type/event_type/data/headers)
|
||||
--flatten 结构化格式输出稳定的顶层业务字段,适合 Agent / 脚本直接消费
|
||||
|
||||
默认使用当前 OAuth 登录态自动创建/复用个人订阅并建立个人长连接;非默认组织加
|
||||
--profile。连上后 stderr 打就绪行 [event] ready,等它出现再读 stdout;停机用
|
||||
@@ -144,6 +149,7 @@ SIGTERM、关 stdin,或先用 dws event stop <subscribe_id> --dry-run 预览
|
||||
}
|
||||
if as == "user" {
|
||||
personalOpts.EventKey = firstArg(args)
|
||||
personalOpts.Flatten = flatten
|
||||
personalOpts.Common = commonConsumeOptions{
|
||||
EventTypes: eventTypes,
|
||||
Filter: filter,
|
||||
@@ -167,6 +173,7 @@ SIGTERM、关 stdin,或先用 dws event stop <subscribe_id> --dry-run 预览
|
||||
return fmt.Errorf("event consume: --debug-raw-events is only supported with --as user")
|
||||
}
|
||||
if err := rejectChangedFlags(c, "user",
|
||||
"flatten",
|
||||
"subscribe-id",
|
||||
"rule",
|
||||
"name",
|
||||
@@ -280,6 +287,8 @@ SIGTERM、关 stdin,或先用 dws event stop <subscribe_id> --dry-run 预览
|
||||
"提示 bus 客户端期望 compact 渲染(语义透传,bus 仍按原 payload 投递)")
|
||||
f.StringVarP(&formatRaw, "format", "f", "ndjson",
|
||||
"输出格式 (ndjson/json/pretty/raw/compact);事件流默认 ndjson")
|
||||
f.BoolVar(&flatten, "flatten", false,
|
||||
"将个人事件 transport envelope 投影为稳定的顶层业务字段")
|
||||
f.StringVar(&outputDir, "output-dir", "",
|
||||
"每事件写一个文件到该目录 ({type}_{id}_{ts}.json);与 stdout 互斥")
|
||||
f.StringArrayVar(&routesRaw, "route", nil,
|
||||
|
||||
@@ -61,6 +61,7 @@ type commonConsumeOptions struct {
|
||||
type personalConsumeOptions struct {
|
||||
Common commonConsumeOptions
|
||||
EventKey string
|
||||
Flatten bool
|
||||
DebugRawEvents bool
|
||||
SubscribeID string
|
||||
Rule string
|
||||
@@ -140,6 +141,7 @@ var (
|
||||
func newEventSchemaCommand() *cobra.Command {
|
||||
var asIdentity string
|
||||
var formatRaw string
|
||||
var flatten bool
|
||||
cmd := &cobra.Command{
|
||||
Use: "schema <event_key>",
|
||||
Short: "显示事件 schema",
|
||||
@@ -157,11 +159,12 @@ func newEventSchemaCommand() *cobra.Command {
|
||||
if !def.Public {
|
||||
return personal.PublicAvailabilityError(args[0])
|
||||
}
|
||||
return renderPersonalSchema(c.OutOrStdout(), def, formatRaw)
|
||||
return renderPersonalSchema(c.OutOrStdout(), def, formatRaw, flatten)
|
||||
},
|
||||
}
|
||||
cmd.Flags().StringVar(&asIdentity, "as", "user", "事件身份: user")
|
||||
cmd.Flags().StringVarP(&formatRaw, "format", "f", "json", "输出格式: json")
|
||||
cmd.Flags().BoolVar(&flatten, "flatten", false, "显示 --flatten 消费模式对应的顶层业务字段 schema")
|
||||
hideEventInternalFlags(cmd, "as")
|
||||
cli.AnnotateRuntimePositionals(cmd, cli.RuntimeSchemaPositional{
|
||||
Name: "event_key",
|
||||
@@ -189,7 +192,7 @@ func runPersonalEventList(c *cobra.Command, opts personalListOptions) error {
|
||||
return tw.Flush()
|
||||
}
|
||||
|
||||
func renderPersonalSchema(w io.Writer, def personal.Definition, format string) error {
|
||||
func renderPersonalSchema(w io.Writer, def personal.Definition, format string, flatten bool) error {
|
||||
format = strings.ToLower(strings.TrimSpace(format))
|
||||
if format == "" {
|
||||
format = "json"
|
||||
@@ -199,7 +202,7 @@ func renderPersonalSchema(w io.Writer, def personal.Definition, format string) e
|
||||
}
|
||||
enc := json.NewEncoder(w)
|
||||
enc.SetIndent("", " ")
|
||||
return enc.Encode(personal.BuildSchemaDocument(def))
|
||||
return enc.Encode(personal.BuildSchemaDocumentForMode(def, flatten))
|
||||
}
|
||||
|
||||
func runPersonalEventConsume(c *cobra.Command, opts personalConsumeOptions) error {
|
||||
@@ -207,6 +210,19 @@ func runPersonalEventConsume(c *cobra.Command, opts personalConsumeOptions) erro
|
||||
if err := ensurePublicPersonalEvent(opts.EventKey); err != nil {
|
||||
return err
|
||||
}
|
||||
rawFormat := ""
|
||||
if f := c.Flags().Lookup("format"); f != nil && f.Changed {
|
||||
rawFormat = opts.Common.FormatRaw
|
||||
}
|
||||
normalised, fellback := consume.NormalizeFormat(rawFormat)
|
||||
if fellback && !opts.Common.Quiet {
|
||||
fmt.Fprintf(c.ErrOrStderr(), "WARN: --format %q has no meaning for event stream; using ndjson\n", rawFormat)
|
||||
}
|
||||
if err := validatePersonalEventOutputMode(opts.Flatten, opts.DebugRawEvents, normalised); err != nil {
|
||||
return fmt.Errorf("event consume --as user: %w", err)
|
||||
}
|
||||
projector := personalEventProjector(opts.DebugRawEvents, opts.Flatten)
|
||||
|
||||
configDir := defaultConfigDir()
|
||||
identity, err := personalResolveEventIdentity(ctx, configDir, opts.StreamSourceID)
|
||||
if err != nil {
|
||||
@@ -221,16 +237,6 @@ func runPersonalEventConsume(c *cobra.Command, opts personalConsumeOptions) erro
|
||||
if err != nil {
|
||||
return fmt.Errorf("event consume --as user: %w", err)
|
||||
}
|
||||
rawFormat := ""
|
||||
if f := c.Flags().Lookup("format"); f != nil && f.Changed {
|
||||
rawFormat = opts.Common.FormatRaw
|
||||
}
|
||||
normalised, fellback := consume.NormalizeFormat(rawFormat)
|
||||
if fellback && !opts.Common.Quiet {
|
||||
fmt.Fprintf(c.ErrOrStderr(), "WARN: --format %q has no meaning for event stream; using ndjson\n", rawFormat)
|
||||
}
|
||||
projector := personalEventProjector(opts.DebugRawEvents)
|
||||
|
||||
if opts.Common.DryRun {
|
||||
if strings.TrimSpace(opts.SubscribeID) == "" {
|
||||
if err := validatePersonalSubscriptionOptions(opts); err != nil {
|
||||
@@ -247,6 +253,7 @@ func runPersonalEventConsume(c *cobra.Command, opts personalConsumeOptions) erro
|
||||
Duration: opts.Common.Duration,
|
||||
EventKey: opts.EventKey,
|
||||
Format: normalised,
|
||||
Flatten: opts.Flatten,
|
||||
OutputDir: opts.Common.OutputDir,
|
||||
Routes: routes,
|
||||
Projector: projector,
|
||||
@@ -303,6 +310,7 @@ func runPersonalEventConsume(c *cobra.Command, opts personalConsumeOptions) erro
|
||||
Duration: opts.Common.Duration,
|
||||
EventKey: eventKey,
|
||||
Format: normalised,
|
||||
Flatten: opts.Flatten,
|
||||
OutputDir: opts.Common.OutputDir,
|
||||
Routes: routes,
|
||||
Projector: projector,
|
||||
@@ -366,11 +374,27 @@ func runPersonalEventConsume(c *cobra.Command, opts personalConsumeOptions) erro
|
||||
return err
|
||||
}
|
||||
|
||||
func personalEventProjector(debugRawEvents bool) consume.Projector {
|
||||
func personalEventProjector(debugRawEvents, flatten bool) consume.Projector {
|
||||
if debugRawEvents {
|
||||
return func(ev transport.Event) (any, error) { return ev, nil }
|
||||
}
|
||||
return personal.ProjectOutput
|
||||
if flatten {
|
||||
return personal.ProjectOutput
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func validatePersonalEventOutputMode(flatten, debugRawEvents bool, format consume.Format) error {
|
||||
if !flatten {
|
||||
return nil
|
||||
}
|
||||
if debugRawEvents {
|
||||
return fmt.Errorf("--flatten and --debug-raw-events are mutually exclusive")
|
||||
}
|
||||
if format == consume.FormatRaw {
|
||||
return fmt.Errorf("--flatten and --format raw are mutually exclusive")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func applyPersonalConsumeFilters(cfg *consume.Config, opts personalConsumeOptions, subscribeID, eventKey string) {
|
||||
|
||||
@@ -20,6 +20,7 @@ import (
|
||||
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/event/consume"
|
||||
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/event/personal"
|
||||
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/event/transport"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
func TestApplyPersonalConsumeFiltersDebugRawEvents(t *testing.T) {
|
||||
@@ -52,11 +53,14 @@ func TestApplyPersonalConsumeFiltersDefault(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestPersonalEventProjectorUsesRawEnvelopeForDebug(t *testing.T) {
|
||||
if personalEventProjector(false) == nil {
|
||||
t.Fatal("normal personal consume projector = nil")
|
||||
func TestPersonalEventProjectorSelectsExplicitModes(t *testing.T) {
|
||||
if personalEventProjector(false, false) != nil {
|
||||
t.Fatal("default personal consume should preserve transport envelope")
|
||||
}
|
||||
projector := personalEventProjector(true)
|
||||
if personalEventProjector(false, true) == nil {
|
||||
t.Fatal("flatten personal consume projector = nil")
|
||||
}
|
||||
projector := personalEventProjector(true, false)
|
||||
if projector == nil {
|
||||
t.Fatal("debug raw personal consume projector = nil")
|
||||
}
|
||||
@@ -74,6 +78,69 @@ func TestPersonalEventProjectorUsesRawEnvelopeForDebug(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestEventConsumeFlattenRejectsRawModesBeforeIdentityResolution(t *testing.T) {
|
||||
for _, tc := range []struct {
|
||||
name string
|
||||
args []string
|
||||
want string
|
||||
}{
|
||||
{
|
||||
name: "raw format",
|
||||
args: []string{personal.EventMention, "--flatten", "--format", "raw"},
|
||||
want: "--flatten and --format raw are mutually exclusive",
|
||||
},
|
||||
{
|
||||
name: "raw debug",
|
||||
args: []string{personal.EventMention, "--flatten", "--debug-raw-events"},
|
||||
want: "--flatten and --debug-raw-events are mutually exclusive",
|
||||
},
|
||||
} {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
t.Setenv("DWS_CONFIG_DIR", t.TempDir())
|
||||
cmd := newEventConsumeCommand()
|
||||
cmd.SilenceUsage = true
|
||||
cmd.SilenceErrors = true
|
||||
cmd.SetArgs(tc.args)
|
||||
err := cmd.Execute()
|
||||
if err == nil || !strings.Contains(err.Error(), tc.want) {
|
||||
t.Fatalf("Execute() error = %v, want %q", err, tc.want)
|
||||
}
|
||||
if strings.Contains(err.Error(), "login") || strings.Contains(err.Error(), "token") {
|
||||
t.Fatalf("output-mode validation ran after identity resolution: %v", err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidatePersonalEventOutputModeAllowsFlattenStructuredFormats(t *testing.T) {
|
||||
for _, format := range []consume.Format{consume.FormatNDJSON, consume.FormatJSON, consume.FormatPretty, consume.FormatCompact} {
|
||||
if err := validatePersonalEventOutputMode(true, false, format); err != nil {
|
||||
t.Fatalf("validatePersonalEventOutputMode(true, false, %q) error = %v", format, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestEventConsumeFlattenFlagIsForwarded(t *testing.T) {
|
||||
oldRun := eventRunPersonalConsume
|
||||
t.Cleanup(func() { eventRunPersonalConsume = oldRun })
|
||||
|
||||
var got personalConsumeOptions
|
||||
eventRunPersonalConsume = func(_ *cobra.Command, opts personalConsumeOptions) error {
|
||||
got = opts
|
||||
return nil
|
||||
}
|
||||
cmd := newEventConsumeCommand()
|
||||
cmd.SilenceUsage = true
|
||||
cmd.SilenceErrors = true
|
||||
cmd.SetArgs([]string{personal.EventMention, "--flatten", "--format", "compact"})
|
||||
if err := cmd.Execute(); err != nil {
|
||||
t.Fatalf("Execute() error = %v", err)
|
||||
}
|
||||
if !got.Flatten || got.Common.FormatRaw != "compact" {
|
||||
t.Fatalf("forwarded options = %#v", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestEventConsumeDebugRawEventsRequiresUserMode(t *testing.T) {
|
||||
cmd := newEventConsumeCommand()
|
||||
cmd.SilenceUsage = true
|
||||
|
||||
@@ -188,7 +188,44 @@ func TestPersonalEventSchemaHidesSchemaIDs(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestPersonalEventSchemaUsesSingleJSONSchema(t *testing.T) {
|
||||
func TestPersonalEventSchemaDefaultsToTransportEnvelope(t *testing.T) {
|
||||
cmd := newEventSchemaCommand()
|
||||
cmd.SilenceUsage = true
|
||||
cmd.SilenceErrors = true
|
||||
var out bytes.Buffer
|
||||
cmd.SetOut(&out)
|
||||
cmd.SetArgs([]string{personal.EventSingleChat})
|
||||
if err := cmd.Execute(); err != nil {
|
||||
t.Fatalf("Execute() error = %v", err)
|
||||
}
|
||||
var doc map[string]any
|
||||
if err := json.Unmarshal(out.Bytes(), &doc); err != nil {
|
||||
t.Fatalf("schema output is not JSON: %v\n%s", err, out.String())
|
||||
}
|
||||
if doc["jq_root_path"] != ".data | fromjson" {
|
||||
t.Fatalf("jq_root_path = %#v, want .data | fromjson", doc["jq_root_path"])
|
||||
}
|
||||
schema, ok := doc["schema"].(map[string]any)
|
||||
if !ok {
|
||||
t.Fatalf("schema = %#v, want object", doc["schema"])
|
||||
}
|
||||
props, ok := schema["properties"].(map[string]any)
|
||||
if !ok {
|
||||
t.Fatalf("schema.properties = %#v, want object", schema["properties"])
|
||||
}
|
||||
for _, field := range []string{"type", "seq", "event_type", "data", "headers", "subscribe_id"} {
|
||||
if _, ok := props[field]; !ok {
|
||||
t.Fatalf("default envelope schema missing %q: %#v", field, props)
|
||||
}
|
||||
}
|
||||
for _, field := range []string{"content", "sender", "conversation_id", "timestamp"} {
|
||||
if _, ok := props[field]; ok {
|
||||
t.Fatalf("default envelope schema unexpectedly contains flat field %q", field)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestPersonalEventFlattenedSchemaUsesSingleJSONSchema(t *testing.T) {
|
||||
for _, eventKey := range []string{
|
||||
personal.EventMention,
|
||||
personal.EventSingleChat,
|
||||
@@ -200,7 +237,7 @@ func TestPersonalEventSchemaUsesSingleJSONSchema(t *testing.T) {
|
||||
cmd.SilenceErrors = true
|
||||
var out bytes.Buffer
|
||||
cmd.SetOut(&out)
|
||||
cmd.SetArgs([]string{eventKey})
|
||||
cmd.SetArgs([]string{eventKey, "--flatten"})
|
||||
if err := cmd.Execute(); err != nil {
|
||||
t.Fatalf("Execute() error = %v", err)
|
||||
}
|
||||
@@ -316,7 +353,7 @@ func TestPersonalActionEventSchemaMatchesFlatOutput(t *testing.T) {
|
||||
cmd.SilenceErrors = true
|
||||
var out bytes.Buffer
|
||||
cmd.SetOut(&out)
|
||||
cmd.SetArgs([]string{eventKey})
|
||||
cmd.SetArgs([]string{eventKey, "--flatten"})
|
||||
if err := cmd.Execute(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -367,6 +404,9 @@ func TestEventSchemaDefaultsToUser(t *testing.T) {
|
||||
if doc["event_key"] != personal.EventSingleChat {
|
||||
t.Fatalf("event_key = %#v, want %s", doc["event_key"], personal.EventSingleChat)
|
||||
}
|
||||
if doc["jq_root_path"] != ".data | fromjson" {
|
||||
t.Fatalf("jq_root_path = %#v, want default envelope path", doc["jq_root_path"])
|
||||
}
|
||||
}
|
||||
|
||||
func TestPersonalEventFromUserIsPubliclyAvailable(t *testing.T) {
|
||||
@@ -469,6 +509,9 @@ func TestEventConsumeCobraSchemaIncludesOpenDingTalkID(t *testing.T) {
|
||||
if _, ok := params["odid"]; ok {
|
||||
t.Fatalf("schema parameters unexpectedly include odid alias: %#v", params)
|
||||
}
|
||||
if _, ok := params["flatten"]; !ok {
|
||||
t.Fatalf("schema parameters missing flatten: %#v", params)
|
||||
}
|
||||
for _, name := range []string{"user", "open-dingtalk-id", "group"} {
|
||||
param, ok := params[name].(map[string]any)
|
||||
if !ok {
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
# Schema Runtime and Publication Agent Guide
|
||||
|
||||
This file applies to `internal/cli/`. Read
|
||||
[`docs/schema-contributor-guide.md`](../../docs/schema-contributor-guide.md)
|
||||
before editing.
|
||||
|
||||
## Owning inputs
|
||||
|
||||
| Change | Edit |
|
||||
|---|---|
|
||||
| Canonical identity, primary path, aliases, navigation | `schema_command_registry.json` |
|
||||
| Parameter/property mapping | `schema_parameter_bindings.json` or reviewed metadata overlay |
|
||||
| Safety, interface, runtime gate | `schema_hints/metadata/<product>.json` |
|
||||
| Agent selection and examples | `schema_hints/selection/<product>.json` |
|
||||
| Exact reviewed omission | `schema_command_exclusions.json` |
|
||||
| Runtime query/projection behavior | Go implementation and focused tests in this package |
|
||||
|
||||
The executable Cobra tree outside this package owns whether a command exists
|
||||
and which flags it accepts. Do not create a command or flag in Schema inputs.
|
||||
|
||||
## Generated boundary
|
||||
|
||||
- `schema_catalog.json` and `schema_agent_metadata/` are generated outputs.
|
||||
- Never hand-edit, merge from, or use a previous generated output as an input.
|
||||
- Change the owning reviewed input or generator, run `make generate-schema`,
|
||||
and inspect authored and generated diffs separately.
|
||||
- A broad unrelated generated diff is a failure signal, not acceptable churn.
|
||||
|
||||
## Self-check
|
||||
|
||||
- Every public runnable Cobra leaf is bound or has one exact reviewed
|
||||
exclusion; every delivered tool binds back to a runnable leaf.
|
||||
- Registry identity and native annotations agree; aliases do not mutate the
|
||||
resolved contract.
|
||||
- Parameters reference real flags and retain Cobra-required floors.
|
||||
- Selection examples use executable paths/flags and never include `--yes`.
|
||||
- Runtime confirmation gates and published safety metadata agree.
|
||||
- Full, compact, summary, alias, and Catalog projections derive from the same
|
||||
typed `ToolSpec` and preserve provenance winners.
|
||||
|
||||
Run the focused package/generator tests and the complete command list in
|
||||
`docs/schema-contributor-guide.md`, beginning with `make generate-schema`.
|
||||
@@ -2,7 +2,7 @@
|
||||
"product_id": "event",
|
||||
"tools": {
|
||||
"event consume": {
|
||||
"agent_summary": "订阅并持续消费指定个人事件,输出 NDJSON 事件流",
|
||||
"agent_summary": "订阅并持续消费指定个人事件;Agent 使用 --flatten 输出顶层业务 NDJSON",
|
||||
"agent_summary_source": "dws-agent-selection/event",
|
||||
"availability": "available",
|
||||
"avoid_when": [
|
||||
@@ -13,19 +13,19 @@
|
||||
"effect": "write",
|
||||
"effect_source": "agent-hint",
|
||||
"examples": [
|
||||
"dws event consume user_im_message_receive_user --open-dingtalk-id open-example --max-events 1 --format ndjson",
|
||||
"dws event consume user_im_message_reaction_group --group cid-example --max-events 1 --format ndjson"
|
||||
"dws event consume user_im_message_receive_user --open-dingtalk-id open-example --flatten --max-events 1 --format ndjson",
|
||||
"dws event consume user_im_message_reaction_group --group cid-example --flatten --max-events 1 --format ndjson"
|
||||
],
|
||||
"field_provenance": {
|
||||
"agent_summary": {
|
||||
"value": "订阅并持续消费指定个人事件,输出 NDJSON 事件流",
|
||||
"value": "订阅并持续消费指定个人事件;Agent 使用 --flatten 输出顶层业务 NDJSON",
|
||||
"source": "internal/cli/schema_hints/selection/event.json",
|
||||
"precedence": "reviewed_explicit",
|
||||
"resolution": "highest_precedence",
|
||||
"review_reason": "人工依据实时 dws schema(或 Skill/Cobra/pinned MCP 对照)决策化选型文案与门禁;不改写命令身份与参数契约;示例不含 --yes。",
|
||||
"candidates": [
|
||||
{
|
||||
"value": "订阅并持续消费指定个人事件,输出 NDJSON 事件流",
|
||||
"value": "订阅并持续消费指定个人事件;Agent 使用 --flatten 输出顶层业务 NDJSON",
|
||||
"source": "internal/cli/schema_hints/selection/event.json",
|
||||
"precedence": "reviewed_explicit",
|
||||
"selected": true,
|
||||
@@ -105,8 +105,8 @@
|
||||
},
|
||||
"examples": {
|
||||
"value": [
|
||||
"dws event consume user_im_message_receive_user --open-dingtalk-id open-example --max-events 1 --format ndjson",
|
||||
"dws event consume user_im_message_reaction_group --group cid-example --max-events 1 --format ndjson"
|
||||
"dws event consume user_im_message_receive_user --open-dingtalk-id open-example --flatten --max-events 1 --format ndjson",
|
||||
"dws event consume user_im_message_reaction_group --group cid-example --flatten --max-events 1 --format ndjson"
|
||||
],
|
||||
"source": "internal/cli/schema_hints/selection/event.json",
|
||||
"precedence": "reviewed_explicit",
|
||||
@@ -115,8 +115,8 @@
|
||||
"candidates": [
|
||||
{
|
||||
"value": [
|
||||
"dws event consume user_im_message_receive_user --open-dingtalk-id open-example --max-events 1 --format ndjson",
|
||||
"dws event consume user_im_message_reaction_group --group cid-example --max-events 1 --format ndjson"
|
||||
"dws event consume user_im_message_receive_user --open-dingtalk-id open-example --flatten --max-events 1 --format ndjson",
|
||||
"dws event consume user_im_message_reaction_group --group cid-example --flatten --max-events 1 --format ndjson"
|
||||
],
|
||||
"source": "internal/cli/schema_hints/selection/event.json",
|
||||
"precedence": "reviewed_explicit",
|
||||
@@ -538,7 +538,7 @@
|
||||
]
|
||||
},
|
||||
"event schema": {
|
||||
"agent_summary": "查询指定个人事件码的 payload 字段结构",
|
||||
"agent_summary": "查询指定个人事件码的输出字段结构;Agent 应查询 --flatten 模式",
|
||||
"agent_summary_source": "dws-agent-selection/event",
|
||||
"availability": "available",
|
||||
"avoid_when": [
|
||||
@@ -549,18 +549,18 @@
|
||||
"effect": "read",
|
||||
"effect_source": "agent-hint",
|
||||
"examples": [
|
||||
"dws event schema user_im_message_receive_at --format json"
|
||||
"dws event schema user_im_message_receive_at --flatten --format json"
|
||||
],
|
||||
"field_provenance": {
|
||||
"agent_summary": {
|
||||
"value": "查询指定个人事件码的 payload 字段结构",
|
||||
"value": "查询指定个人事件码的输出字段结构;Agent 应查询 --flatten 模式",
|
||||
"source": "internal/cli/schema_hints/selection/event.json",
|
||||
"precedence": "reviewed_explicit",
|
||||
"resolution": "highest_precedence",
|
||||
"review_reason": "人工依据实时 dws schema(或 Skill/Cobra/pinned MCP 对照)决策化选型文案与门禁;不改写命令身份与参数契约;示例不含 --yes。",
|
||||
"candidates": [
|
||||
{
|
||||
"value": "查询指定个人事件码的 payload 字段结构",
|
||||
"value": "查询指定个人事件码的输出字段结构;Agent 应查询 --flatten 模式",
|
||||
"source": "internal/cli/schema_hints/selection/event.json",
|
||||
"precedence": "reviewed_explicit",
|
||||
"selected": true,
|
||||
@@ -640,7 +640,7 @@
|
||||
},
|
||||
"examples": {
|
||||
"value": [
|
||||
"dws event schema user_im_message_receive_at --format json"
|
||||
"dws event schema user_im_message_receive_at --flatten --format json"
|
||||
],
|
||||
"source": "internal/cli/schema_hints/selection/event.json",
|
||||
"precedence": "reviewed_explicit",
|
||||
@@ -649,7 +649,7 @@
|
||||
"candidates": [
|
||||
{
|
||||
"value": [
|
||||
"dws event schema user_im_message_receive_at --format json"
|
||||
"dws event schema user_im_message_receive_at --flatten --format json"
|
||||
],
|
||||
"source": "internal/cli/schema_hints/selection/event.json",
|
||||
"precedence": "reviewed_explicit",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"version": 1,
|
||||
"source_hash": "sha256:de587cba5051dd4c2715353012d8d9f2a7c5208a0c6dee4ea703cfc97b7351f0",
|
||||
"source_hash": "sha256:5df496973c41b3b4f7ae8c856ff0e9e99bcabd4455419b7d41bb23c44df6f1af",
|
||||
"surface_hash": "sha256:7ef588f38052f0104e027c8daff5fefd68698781f2c87715461183d4058288ed",
|
||||
"coverage": {
|
||||
"surface_products": 22,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"version": 1,
|
||||
"source_hash": "sha256:de587cba5051dd4c2715353012d8d9f2a7c5208a0c6dee4ea703cfc97b7351f0",
|
||||
"source_hash": "sha256:5df496973c41b3b4f7ae8c856ff0e9e99bcabd4455419b7d41bb23c44df6f1af",
|
||||
"surface_hash": "sha256:7ef588f38052f0104e027c8daff5fefd68698781f2c87715461183d4058288ed",
|
||||
"source_files": 150,
|
||||
"hint_files": 46,
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"version": 1,
|
||||
"source_hash": "sha256:522d4b43cf13f07430a407319a772cd11e9b1f268f8d98b4d68bdb385e4e585b",
|
||||
"source_hash": "sha256:14398788381c822f208e8cae059d98cd60da6625023869c74015735650d67e6f",
|
||||
"surface_hash": "sha256:7ef588f38052f0104e027c8daff5fefd68698781f2c87715461183d4058288ed",
|
||||
"catalog": {
|
||||
"agent_metadata": {
|
||||
"products_with_metadata": 22,
|
||||
"source": "embedded-skill-metadata",
|
||||
"source_hash": "sha256:de587cba5051dd4c2715353012d8d9f2a7c5208a0c6dee4ea703cfc97b7351f0",
|
||||
"source_hash": "sha256:5df496973c41b3b4f7ae8c856ff0e9e99bcabd4455419b7d41bb23c44df6f1af",
|
||||
"surface_hash": "sha256:7ef588f38052f0104e027c8daff5fefd68698781f2c87715461183d4058288ed",
|
||||
"surface_products": 22,
|
||||
"surface_tools": 572,
|
||||
@@ -12178,7 +12178,7 @@
|
||||
"tools": [
|
||||
{
|
||||
"agent_metadata_source": "embedded-skill-metadata",
|
||||
"agent_summary": "订阅并持续消费指定个人事件,输出 NDJSON 事件流",
|
||||
"agent_summary": "订阅并持续消费指定个人事件;Agent 使用 --flatten 输出顶层业务 NDJSON",
|
||||
"agent_summary_source": "dws-agent-selection/event",
|
||||
"availability": "available",
|
||||
"avoid_when": [
|
||||
@@ -12189,7 +12189,7 @@
|
||||
"cli_name": "consume",
|
||||
"cli_path": "event consume",
|
||||
"confirmation": "not_required",
|
||||
"description": "订阅 DingTalk 个人事件并将每条事件以 NDJSON 输出到 stdout。\n\n输出格式(事件流默认 ndjson;显式 -f json/pretty/raw 可覆盖;-f table/csv 对\n事件流无意义会 fallback 到 ndjson):\n ndjson (默认) 一行一对象,适合 jq / 管道处理\n json 每事件多行美化 JSON(必须配 --max-events 或 --duration)\n pretty 同 json,未来加颜色\n raw 仅 SDK 原始 payload,无外层封装\n compact 扁平化 + 解析嵌套 + 抽取语义字段(Agent 友好)\n\n默认使用当前 OAuth 登录态自动创建/复用个人订阅并建立个人长连接;非默认组织加\n--profile。连上后 stderr 打就绪行 [event] ready,等它出现再读 stdout;停机用\nSIGTERM、关 stdin,或先用 dws event stop \u003csubscribe_id\u003e --dry-run 预览、确认后加\n--yes,绝不要 kill -9。\n--event-types/--filter 只影响本地 bus → consume 这一段投递;普通个人事件消费\n通常不需要设置。",
|
||||
"description": "订阅 DingTalk 个人事件并将每条事件以 NDJSON 输出到 stdout。\n\n输出格式(事件流默认 ndjson;显式 -f json/pretty/raw 可覆盖;-f table/csv 对\n事件流无意义会 fallback 到 ndjson):\n ndjson (默认) 一行一对象,适合 jq / 管道处理\n json 每事件多行美化 JSON(必须配 --max-events 或 --duration)\n pretty 同 json,未来加颜色\n raw 仅 SDK 原始 payload,无外层封装\n compact 单行紧凑 JSON;不传 --flatten 时沿用原 compact processor\n\n数据结构:\n ndjson/json/pretty 默认保持 transport envelope(type/event_type/data/headers)\n --flatten 结构化格式输出稳定的顶层业务字段,适合 Agent / 脚本直接消费\n\n默认使用当前 OAuth 登录态自动创建/复用个人订阅并建立个人长连接;非默认组织加\n--profile。连上后 stderr 打就绪行 [event] ready,等它出现再读 stdout;停机用\nSIGTERM、关 stdin,或先用 dws event stop \u003csubscribe_id\u003e --dry-run 预览、确认后加\n--yes,绝不要 kill -9。\n--event-types/--filter 只影响本地 bus → consume 这一段投递;普通个人事件消费\n通常不需要设置。",
|
||||
"effect": "write",
|
||||
"idempotency": "non_idempotent",
|
||||
"interface_mode": "composite",
|
||||
@@ -12234,7 +12234,7 @@
|
||||
},
|
||||
{
|
||||
"agent_metadata_source": "embedded-skill-metadata",
|
||||
"agent_summary": "查询指定个人事件码的 payload 字段结构",
|
||||
"agent_summary": "查询指定个人事件码的输出字段结构;Agent 应查询 --flatten 模式",
|
||||
"agent_summary_source": "dws-agent-selection/event",
|
||||
"availability": "available",
|
||||
"avoid_when": [
|
||||
@@ -290130,7 +290130,7 @@
|
||||
"skills/mono/SKILL.md",
|
||||
"skills/mono/references/products/event.md"
|
||||
],
|
||||
"agent_summary": "订阅并持续消费指定个人事件,输出 NDJSON 事件流",
|
||||
"agent_summary": "订阅并持续消费指定个人事件;Agent 使用 --flatten 输出顶层业务 NDJSON",
|
||||
"agent_summary_source": "dws-agent-selection/event",
|
||||
"availability": "available",
|
||||
"avoid_when": [
|
||||
@@ -290149,13 +290149,13 @@
|
||||
]
|
||||
]
|
||||
},
|
||||
"description": "订阅 DingTalk 个人事件并将每条事件以 NDJSON 输出到 stdout。\n\n输出格式(事件流默认 ndjson;显式 -f json/pretty/raw 可覆盖;-f table/csv 对\n事件流无意义会 fallback 到 ndjson):\n ndjson (默认) 一行一对象,适合 jq / 管道处理\n json 每事件多行美化 JSON(必须配 --max-events 或 --duration)\n pretty 同 json,未来加颜色\n raw 仅 SDK 原始 payload,无外层封装\n compact 扁平化 + 解析嵌套 + 抽取语义字段(Agent 友好)\n\n默认使用当前 OAuth 登录态自动创建/复用个人订阅并建立个人长连接;非默认组织加\n--profile。连上后 stderr 打就绪行 [event] ready,等它出现再读 stdout;停机用\nSIGTERM、关 stdin,或先用 dws event stop \u003csubscribe_id\u003e --dry-run 预览、确认后加\n--yes,绝不要 kill -9。\n--event-types/--filter 只影响本地 bus → consume 这一段投递;普通个人事件消费\n通常不需要设置。",
|
||||
"description": "订阅 DingTalk 个人事件并将每条事件以 NDJSON 输出到 stdout。\n\n输出格式(事件流默认 ndjson;显式 -f json/pretty/raw 可覆盖;-f table/csv 对\n事件流无意义会 fallback 到 ndjson):\n ndjson (默认) 一行一对象,适合 jq / 管道处理\n json 每事件多行美化 JSON(必须配 --max-events 或 --duration)\n pretty 同 json,未来加颜色\n raw 仅 SDK 原始 payload,无外层封装\n compact 单行紧凑 JSON;不传 --flatten 时沿用原 compact processor\n\n数据结构:\n ndjson/json/pretty 默认保持 transport envelope(type/event_type/data/headers)\n --flatten 结构化格式输出稳定的顶层业务字段,适合 Agent / 脚本直接消费\n\n默认使用当前 OAuth 登录态自动创建/复用个人订阅并建立个人长连接;非默认组织加\n--profile。连上后 stderr 打就绪行 [event] ready,等它出现再读 stdout;停机用\nSIGTERM、关 stdin,或先用 dws event stop \u003csubscribe_id\u003e --dry-run 预览、确认后加\n--yes,绝不要 kill -9。\n--event-types/--filter 只影响本地 bus → consume 这一段投递;普通个人事件消费\n通常不需要设置。",
|
||||
"display": "事件订阅 (DingTalk Stream 长连接)",
|
||||
"effect": "write",
|
||||
"effect_source": "agent-hint",
|
||||
"examples": [
|
||||
"dws event consume user_im_message_receive_user --open-dingtalk-id open-example --max-events 1 --format ndjson",
|
||||
"dws event consume user_im_message_reaction_group --group cid-example --max-events 1 --format ndjson"
|
||||
"dws event consume user_im_message_receive_user --open-dingtalk-id open-example --flatten --max-events 1 --format ndjson",
|
||||
"dws event consume user_im_message_reaction_group --group cid-example --flatten --max-events 1 --format ndjson"
|
||||
],
|
||||
"field_provenance": {
|
||||
"agent_summary": {
|
||||
@@ -290165,14 +290165,14 @@
|
||||
"review_reason": "人工依据实时 dws schema(或 Skill/Cobra/pinned MCP 对照)决策化选型文案与门禁;不改写命令身份与参数契约;示例不含 --yes。",
|
||||
"selected": true,
|
||||
"source": "internal/cli/schema_hints/selection/event.json",
|
||||
"value": "订阅并持续消费指定个人事件,输出 NDJSON 事件流"
|
||||
"value": "订阅并持续消费指定个人事件;Agent 使用 --flatten 输出顶层业务 NDJSON"
|
||||
}
|
||||
],
|
||||
"precedence": "reviewed_explicit",
|
||||
"resolution": "highest_precedence",
|
||||
"review_reason": "人工依据实时 dws schema(或 Skill/Cobra/pinned MCP 对照)决策化选型文案与门禁;不改写命令身份与参数契约;示例不含 --yes。",
|
||||
"source": "internal/cli/schema_hints/selection/event.json",
|
||||
"value": "订阅并持续消费指定个人事件,输出 NDJSON 事件流"
|
||||
"value": "订阅并持续消费指定个人事件;Agent 使用 --flatten 输出顶层业务 NDJSON"
|
||||
},
|
||||
"availability": {
|
||||
"candidates": [
|
||||
@@ -290250,13 +290250,13 @@
|
||||
"precedence": "cobra_help",
|
||||
"selected": true,
|
||||
"source": "cobra_help",
|
||||
"value": "订阅 DingTalk 个人事件并将每条事件以 NDJSON 输出到 stdout。\n\n输出格式(事件流默认 ndjson;显式 -f json/pretty/raw 可覆盖;-f table/csv 对\n事件流无意义会 fallback 到 ndjson):\n ndjson (默认) 一行一对象,适合 jq / 管道处理\n json 每事件多行美化 JSON(必须配 --max-events 或 --duration)\n pretty 同 json,未来加颜色\n raw 仅 SDK 原始 payload,无外层封装\n compact 扁平化 + 解析嵌套 + 抽取语义字段(Agent 友好)\n\n默认使用当前 OAuth 登录态自动创建/复用个人订阅并建立个人长连接;非默认组织加\n--profile。连上后 stderr 打就绪行 [event] ready,等它出现再读 stdout;停机用\nSIGTERM、关 stdin,或先用 dws event stop \u003csubscribe_id\u003e --dry-run 预览、确认后加\n--yes,绝不要 kill -9。\n--event-types/--filter 只影响本地 bus → consume 这一段投递;普通个人事件消费\n通常不需要设置。"
|
||||
"value": "订阅 DingTalk 个人事件并将每条事件以 NDJSON 输出到 stdout。\n\n输出格式(事件流默认 ndjson;显式 -f json/pretty/raw 可覆盖;-f table/csv 对\n事件流无意义会 fallback 到 ndjson):\n ndjson (默认) 一行一对象,适合 jq / 管道处理\n json 每事件多行美化 JSON(必须配 --max-events 或 --duration)\n pretty 同 json,未来加颜色\n raw 仅 SDK 原始 payload,无外层封装\n compact 单行紧凑 JSON;不传 --flatten 时沿用原 compact processor\n\n数据结构:\n ndjson/json/pretty 默认保持 transport envelope(type/event_type/data/headers)\n --flatten 结构化格式输出稳定的顶层业务字段,适合 Agent / 脚本直接消费\n\n默认使用当前 OAuth 登录态自动创建/复用个人订阅并建立个人长连接;非默认组织加\n--profile。连上后 stderr 打就绪行 [event] ready,等它出现再读 stdout;停机用\nSIGTERM、关 stdin,或先用 dws event stop \u003csubscribe_id\u003e --dry-run 预览、确认后加\n--yes,绝不要 kill -9。\n--event-types/--filter 只影响本地 bus → consume 这一段投递;普通个人事件消费\n通常不需要设置。"
|
||||
}
|
||||
],
|
||||
"precedence": "cobra_help",
|
||||
"resolution": "highest_precedence",
|
||||
"source": "cobra_help",
|
||||
"value": "订阅 DingTalk 个人事件并将每条事件以 NDJSON 输出到 stdout。\n\n输出格式(事件流默认 ndjson;显式 -f json/pretty/raw 可覆盖;-f table/csv 对\n事件流无意义会 fallback 到 ndjson):\n ndjson (默认) 一行一对象,适合 jq / 管道处理\n json 每事件多行美化 JSON(必须配 --max-events 或 --duration)\n pretty 同 json,未来加颜色\n raw 仅 SDK 原始 payload,无外层封装\n compact 扁平化 + 解析嵌套 + 抽取语义字段(Agent 友好)\n\n默认使用当前 OAuth 登录态自动创建/复用个人订阅并建立个人长连接;非默认组织加\n--profile。连上后 stderr 打就绪行 [event] ready,等它出现再读 stdout;停机用\nSIGTERM、关 stdin,或先用 dws event stop \u003csubscribe_id\u003e --dry-run 预览、确认后加\n--yes,绝不要 kill -9。\n--event-types/--filter 只影响本地 bus → consume 这一段投递;普通个人事件消费\n通常不需要设置。"
|
||||
"value": "订阅 DingTalk 个人事件并将每条事件以 NDJSON 输出到 stdout。\n\n输出格式(事件流默认 ndjson;显式 -f json/pretty/raw 可覆盖;-f table/csv 对\n事件流无意义会 fallback 到 ndjson):\n ndjson (默认) 一行一对象,适合 jq / 管道处理\n json 每事件多行美化 JSON(必须配 --max-events 或 --duration)\n pretty 同 json,未来加颜色\n raw 仅 SDK 原始 payload,无外层封装\n compact 单行紧凑 JSON;不传 --flatten 时沿用原 compact processor\n\n数据结构:\n ndjson/json/pretty 默认保持 transport envelope(type/event_type/data/headers)\n --flatten 结构化格式输出稳定的顶层业务字段,适合 Agent / 脚本直接消费\n\n默认使用当前 OAuth 登录态自动创建/复用个人订阅并建立个人长连接;非默认组织加\n--profile。连上后 stderr 打就绪行 [event] ready,等它出现再读 stdout;停机用\nSIGTERM、关 stdin,或先用 dws event stop \u003csubscribe_id\u003e --dry-run 预览、确认后加\n--yes,绝不要 kill -9。\n--event-types/--filter 只影响本地 bus → consume 这一段投递;普通个人事件消费\n通常不需要设置。"
|
||||
},
|
||||
"effect": {
|
||||
"candidates": [
|
||||
@@ -290282,8 +290282,8 @@
|
||||
"selected": true,
|
||||
"source": "internal/cli/schema_hints/selection/event.json",
|
||||
"value": [
|
||||
"dws event consume user_im_message_receive_user --open-dingtalk-id open-example --max-events 1 --format ndjson",
|
||||
"dws event consume user_im_message_reaction_group --group cid-example --max-events 1 --format ndjson"
|
||||
"dws event consume user_im_message_receive_user --open-dingtalk-id open-example --flatten --max-events 1 --format ndjson",
|
||||
"dws event consume user_im_message_reaction_group --group cid-example --flatten --max-events 1 --format ndjson"
|
||||
]
|
||||
}
|
||||
],
|
||||
@@ -290292,8 +290292,8 @@
|
||||
"review_reason": "人工依据实时 dws schema(或 Skill/Cobra/pinned MCP 对照)决策化选型文案与门禁;不改写命令身份与参数契约;示例不含 --yes。",
|
||||
"source": "internal/cli/schema_hints/selection/event.json",
|
||||
"value": [
|
||||
"dws event consume user_im_message_receive_user --open-dingtalk-id open-example --max-events 1 --format ndjson",
|
||||
"dws event consume user_im_message_reaction_group --group cid-example --max-events 1 --format ndjson"
|
||||
"dws event consume user_im_message_receive_user --open-dingtalk-id open-example --flatten --max-events 1 --format ndjson",
|
||||
"dws event consume user_im_message_reaction_group --group cid-example --flatten --max-events 1 --format ndjson"
|
||||
]
|
||||
},
|
||||
"idempotency": {
|
||||
@@ -290444,7 +290444,7 @@
|
||||
"interface_reason": "Reviewed composite workflow: the command creates or reuses a remote personal-event subscription and coordinates the local event bus and Stream consumer; no single pinned RPC represents the workflow.",
|
||||
"is_alias": false,
|
||||
"name": "consume",
|
||||
"parameter_count": 27,
|
||||
"parameter_count": 28,
|
||||
"parameters": {
|
||||
"compact": {
|
||||
"description": "提示 bus 客户端期望 compact 渲染(语义透传,bus 仍按原 payload 投递)",
|
||||
@@ -291124,6 +291124,90 @@
|
||||
"required": false,
|
||||
"type": "string"
|
||||
},
|
||||
"flatten": {
|
||||
"description": "将个人事件 transport envelope 投影为稳定的顶层业务字段",
|
||||
"field_provenance": {
|
||||
"description": {
|
||||
"candidates": [
|
||||
{
|
||||
"precedence": "cobra_contract",
|
||||
"selected": true,
|
||||
"source": "cobra_usage",
|
||||
"value": "将个人事件 transport envelope 投影为稳定的顶层业务字段"
|
||||
},
|
||||
{
|
||||
"precedence": "default",
|
||||
"selected": false,
|
||||
"source": "default",
|
||||
"value": ""
|
||||
}
|
||||
],
|
||||
"precedence": "cobra_contract",
|
||||
"resolution": "highest_precedence",
|
||||
"source": "cobra_usage",
|
||||
"value": "将个人事件 transport envelope 投影为稳定的顶层业务字段"
|
||||
},
|
||||
"property": {
|
||||
"candidates": [
|
||||
{
|
||||
"precedence": "inference",
|
||||
"selected": true,
|
||||
"source": "flag_name_inference",
|
||||
"value": "flatten"
|
||||
}
|
||||
],
|
||||
"precedence": "inference",
|
||||
"resolution": "highest_precedence",
|
||||
"source": "flag_name_inference",
|
||||
"value": "flatten"
|
||||
},
|
||||
"required": {
|
||||
"candidates": [
|
||||
{
|
||||
"precedence": "default",
|
||||
"selected": true,
|
||||
"source": "default",
|
||||
"value": false
|
||||
}
|
||||
],
|
||||
"precedence": "default",
|
||||
"resolution": "fallback",
|
||||
"source": "default",
|
||||
"value": false
|
||||
},
|
||||
"required_when": {
|
||||
"candidates": [
|
||||
{
|
||||
"precedence": "default",
|
||||
"selected": true,
|
||||
"source": "default",
|
||||
"value": ""
|
||||
}
|
||||
],
|
||||
"precedence": "default",
|
||||
"resolution": "highest_precedence",
|
||||
"source": "default",
|
||||
"value": ""
|
||||
},
|
||||
"type": {
|
||||
"candidates": [
|
||||
{
|
||||
"precedence": "cobra_contract",
|
||||
"selected": true,
|
||||
"source": "cobra_flag_type",
|
||||
"value": "boolean"
|
||||
}
|
||||
],
|
||||
"precedence": "cobra_contract",
|
||||
"resolution": "highest_precedence",
|
||||
"source": "cobra_flag_type",
|
||||
"value": "boolean"
|
||||
}
|
||||
},
|
||||
"property": "flatten",
|
||||
"required": false,
|
||||
"type": "boolean"
|
||||
},
|
||||
"force": {
|
||||
"description": "仅 --foreground 模式生效:跳过单实例锁 (慎用:会让云事件被随机切分)",
|
||||
"field_provenance": {
|
||||
@@ -293464,7 +293548,7 @@
|
||||
"skills/mono/SKILL.md",
|
||||
"skills/mono/references/products/event.md"
|
||||
],
|
||||
"agent_summary": "查询指定个人事件码的 payload 字段结构",
|
||||
"agent_summary": "查询指定个人事件码的输出字段结构;Agent 应查询 --flatten 模式",
|
||||
"agent_summary_source": "dws-agent-selection/event",
|
||||
"availability": "available",
|
||||
"avoid_when": [
|
||||
@@ -293480,7 +293564,7 @@
|
||||
"effect": "read",
|
||||
"effect_source": "agent-hint",
|
||||
"examples": [
|
||||
"dws event schema user_im_message_receive_at --format json"
|
||||
"dws event schema user_im_message_receive_at --flatten --format json"
|
||||
],
|
||||
"field_provenance": {
|
||||
"agent_summary": {
|
||||
@@ -293490,14 +293574,14 @@
|
||||
"review_reason": "人工依据实时 dws schema(或 Skill/Cobra/pinned MCP 对照)决策化选型文案与门禁;不改写命令身份与参数契约;示例不含 --yes。",
|
||||
"selected": true,
|
||||
"source": "internal/cli/schema_hints/selection/event.json",
|
||||
"value": "查询指定个人事件码的 payload 字段结构"
|
||||
"value": "查询指定个人事件码的输出字段结构;Agent 应查询 --flatten 模式"
|
||||
}
|
||||
],
|
||||
"precedence": "reviewed_explicit",
|
||||
"resolution": "highest_precedence",
|
||||
"review_reason": "人工依据实时 dws schema(或 Skill/Cobra/pinned MCP 对照)决策化选型文案与门禁;不改写命令身份与参数契约;示例不含 --yes。",
|
||||
"source": "internal/cli/schema_hints/selection/event.json",
|
||||
"value": "查询指定个人事件码的 payload 字段结构"
|
||||
"value": "查询指定个人事件码的输出字段结构;Agent 应查询 --flatten 模式"
|
||||
},
|
||||
"availability": {
|
||||
"candidates": [
|
||||
@@ -293607,7 +293691,7 @@
|
||||
"selected": true,
|
||||
"source": "internal/cli/schema_hints/selection/event.json",
|
||||
"value": [
|
||||
"dws event schema user_im_message_receive_at --format json"
|
||||
"dws event schema user_im_message_receive_at --flatten --format json"
|
||||
]
|
||||
}
|
||||
],
|
||||
@@ -293616,7 +293700,7 @@
|
||||
"review_reason": "人工依据实时 dws schema(或 Skill/Cobra/pinned MCP 对照)决策化选型文案与门禁;不改写命令身份与参数契约;示例不含 --yes。",
|
||||
"source": "internal/cli/schema_hints/selection/event.json",
|
||||
"value": [
|
||||
"dws event schema user_im_message_receive_at --format json"
|
||||
"dws event schema user_im_message_receive_at --flatten --format json"
|
||||
]
|
||||
},
|
||||
"idempotency": {
|
||||
@@ -293763,8 +293847,92 @@
|
||||
"interface_reason": "命令读取 CLI 内置的个人事件 payload 定义,不绑定 pinned MCP RPC",
|
||||
"is_alias": false,
|
||||
"name": "schema",
|
||||
"parameter_count": 1,
|
||||
"parameter_count": 2,
|
||||
"parameters": {
|
||||
"flatten": {
|
||||
"description": "显示 --flatten 消费模式对应的顶层业务字段 schema",
|
||||
"field_provenance": {
|
||||
"description": {
|
||||
"candidates": [
|
||||
{
|
||||
"precedence": "cobra_contract",
|
||||
"selected": true,
|
||||
"source": "cobra_usage",
|
||||
"value": "显示 --flatten 消费模式对应的顶层业务字段 schema"
|
||||
},
|
||||
{
|
||||
"precedence": "default",
|
||||
"selected": false,
|
||||
"source": "default",
|
||||
"value": ""
|
||||
}
|
||||
],
|
||||
"precedence": "cobra_contract",
|
||||
"resolution": "highest_precedence",
|
||||
"source": "cobra_usage",
|
||||
"value": "显示 --flatten 消费模式对应的顶层业务字段 schema"
|
||||
},
|
||||
"property": {
|
||||
"candidates": [
|
||||
{
|
||||
"precedence": "inference",
|
||||
"selected": true,
|
||||
"source": "flag_name_inference",
|
||||
"value": "flatten"
|
||||
}
|
||||
],
|
||||
"precedence": "inference",
|
||||
"resolution": "highest_precedence",
|
||||
"source": "flag_name_inference",
|
||||
"value": "flatten"
|
||||
},
|
||||
"required": {
|
||||
"candidates": [
|
||||
{
|
||||
"precedence": "default",
|
||||
"selected": true,
|
||||
"source": "default",
|
||||
"value": false
|
||||
}
|
||||
],
|
||||
"precedence": "default",
|
||||
"resolution": "fallback",
|
||||
"source": "default",
|
||||
"value": false
|
||||
},
|
||||
"required_when": {
|
||||
"candidates": [
|
||||
{
|
||||
"precedence": "default",
|
||||
"selected": true,
|
||||
"source": "default",
|
||||
"value": ""
|
||||
}
|
||||
],
|
||||
"precedence": "default",
|
||||
"resolution": "highest_precedence",
|
||||
"source": "default",
|
||||
"value": ""
|
||||
},
|
||||
"type": {
|
||||
"candidates": [
|
||||
{
|
||||
"precedence": "cobra_contract",
|
||||
"selected": true,
|
||||
"source": "cobra_flag_type",
|
||||
"value": "boolean"
|
||||
}
|
||||
],
|
||||
"precedence": "cobra_contract",
|
||||
"resolution": "highest_precedence",
|
||||
"source": "cobra_flag_type",
|
||||
"value": "boolean"
|
||||
}
|
||||
},
|
||||
"property": "flatten",
|
||||
"required": false,
|
||||
"type": "boolean"
|
||||
},
|
||||
"format": {
|
||||
"default": "json",
|
||||
"description": "输出格式: json",
|
||||
|
||||
@@ -634,6 +634,11 @@
|
||||
"target": "event consume",
|
||||
"reason": "已审查的带 event_key 和输出参数的 Skill 引用,固定映射到当前公开 event consume leaf"
|
||||
},
|
||||
"event consume user_im_message_receive_at": {
|
||||
"status": "alias",
|
||||
"target": "event consume",
|
||||
"reason": "已审查的带 event_key 参数的 Skill 引用,固定映射到当前公开 event consume leaf"
|
||||
},
|
||||
"event consume user_im_message_receive_group": {
|
||||
"status": "alias",
|
||||
"target": "event consume",
|
||||
|
||||
@@ -585,6 +585,11 @@
|
||||
"target": "event consume",
|
||||
"reason": "已审查的带 event_key 和输出参数的 Skill 引用,固定映射到当前公开 event consume leaf"
|
||||
},
|
||||
"event consume user_im_message_receive_at": {
|
||||
"status": "alias",
|
||||
"target": "event consume",
|
||||
"reason": "已审查的带 event_key 参数的 Skill 引用,固定映射到当前公开 event consume leaf"
|
||||
},
|
||||
"event consume user_im_message_receive_group": {
|
||||
"status": "alias",
|
||||
"target": "event consume",
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
},
|
||||
"tools": {
|
||||
"event.consume": {
|
||||
"agent_summary": "订阅并持续消费指定个人事件,输出 NDJSON 事件流",
|
||||
"agent_summary": "订阅并持续消费指定个人事件;Agent 使用 --flatten 输出顶层业务 NDJSON",
|
||||
"use_when": [
|
||||
"需要实时监听 @我、指定单聊、指定群或指定发送人的后续消息事件",
|
||||
"需要监听指定单聊或群聊中的消息已读、撤回或表情回应事件",
|
||||
@@ -20,8 +20,8 @@
|
||||
"只看事件目录/字段时用 event list / event schema"
|
||||
],
|
||||
"examples": [
|
||||
"dws event consume user_im_message_receive_user --open-dingtalk-id open-example --max-events 1 --format ndjson",
|
||||
"dws event consume user_im_message_reaction_group --group cid-example --max-events 1 --format ndjson"
|
||||
"dws event consume user_im_message_receive_user --open-dingtalk-id open-example --flatten --max-events 1 --format ndjson",
|
||||
"dws event consume user_im_message_reaction_group --group cid-example --flatten --max-events 1 --format ndjson"
|
||||
],
|
||||
"reviewed": true,
|
||||
"review_reason": "人工依据实时 dws schema(或 Skill/Cobra/pinned MCP 对照)决策化选型文案与门禁;不改写命令身份与参数契约;示例不含 --yes。",
|
||||
@@ -57,7 +57,7 @@
|
||||
]
|
||||
},
|
||||
"event.schema": {
|
||||
"agent_summary": "查询指定个人事件码的 payload 字段结构",
|
||||
"agent_summary": "查询指定个人事件码的输出字段结构;Agent 应查询 --flatten 模式",
|
||||
"use_when": [
|
||||
"已知任一公开个人消息 event_key,消费前需要理解扁平输出字段"
|
||||
],
|
||||
@@ -66,7 +66,7 @@
|
||||
"要实际收事件时用 event consume"
|
||||
],
|
||||
"examples": [
|
||||
"dws event schema user_im_message_receive_at --format json"
|
||||
"dws event schema user_im_message_receive_at --flatten --format json"
|
||||
],
|
||||
"reviewed": true,
|
||||
"review_reason": "人工依据实时 dws schema(或 Skill/Cobra/pinned MCP 对照)决策化选型文案与门禁;不改写命令身份与参数契约;示例不含 --yes。",
|
||||
|
||||
@@ -90,6 +90,10 @@ type Config struct {
|
||||
// NormalizeFormat; an empty Format here defaults to NDJSON inside
|
||||
// BuildPipeline.
|
||||
Format Format
|
||||
// Flatten records whether the caller explicitly selected a structured
|
||||
// business projection. Projector remains the executable behavior; this
|
||||
// field is surfaced in dry-run output so users can verify the final mode.
|
||||
Flatten bool
|
||||
// OutputDir, if non-empty, switches the fallback sink from stdout to
|
||||
// "file per event" under this directory.
|
||||
OutputDir string
|
||||
|
||||
@@ -132,6 +132,7 @@ func PrintDryRun(w io.Writer, cfg Config) {
|
||||
fmt.Fprintf(w, " filter : %s\n", cfg.Filter)
|
||||
}
|
||||
fmt.Fprintf(w, " format : %s\n", cfg.Format)
|
||||
fmt.Fprintf(w, " flatten : %v\n", cfg.Flatten)
|
||||
if cfg.OutputDir != "" {
|
||||
fmt.Fprintf(w, " output_dir : %s\n", cfg.OutputDir)
|
||||
}
|
||||
|
||||
@@ -137,6 +137,7 @@ func TestPrintDryRun_RendersAllSetFields(t *testing.T) {
|
||||
c.EventTypes = []string{"im.*", "approval.*"}
|
||||
c.Filter = "^im\\."
|
||||
c.Format = FormatCompact
|
||||
c.Flatten = true
|
||||
c.OutputDir = "/tmp/events"
|
||||
c.Routes, _ = ParseRoutes([]string{`^im\.=dir:/tmp/im/`})
|
||||
c.MaxEvents = 5
|
||||
@@ -151,7 +152,7 @@ func TestPrintDryRun_RendersAllSetFields(t *testing.T) {
|
||||
wants := []string{
|
||||
"client_id", "workdir", "ipc_endpoint", "im.*,approval.*",
|
||||
"^im\\.", "compact", "/tmp/events", "route[0]", "max_events : 5",
|
||||
"duration", "true",
|
||||
"duration", "flatten : true", "true",
|
||||
}
|
||||
for _, w := range wants {
|
||||
if !strings.Contains(out, w) {
|
||||
|
||||
@@ -394,6 +394,39 @@ func outputSchema(eventKey string) map[string]any {
|
||||
}
|
||||
}
|
||||
|
||||
func transportEnvelopeSchema(eventKey string) map[string]any {
|
||||
eventType := reflect.TypeOf(transport.Event{})
|
||||
properties := make(map[string]any, eventType.NumField())
|
||||
for i := 0; i < eventType.NumField(); i++ {
|
||||
field := eventType.Field(i)
|
||||
name := strings.Split(field.Tag.Get("json"), ",")[0]
|
||||
property := map[string]any{"type": schemaType(field.Type)}
|
||||
switch name {
|
||||
case "type":
|
||||
property["description"] = "transport frame 类型"
|
||||
property["enum"] = []string{string(transport.FrameTypeEvent)}
|
||||
case "event_type":
|
||||
property["description"] = "事件类型"
|
||||
property["enum"] = []string{eventKey}
|
||||
case "data":
|
||||
property["description"] = "服务端业务 payload JSON 字符串"
|
||||
property["content_media_type"] = "application/json"
|
||||
case "headers":
|
||||
property["description"] = "Stream transport headers"
|
||||
property["additionalProperties"] = map[string]any{"type": "string"}
|
||||
case "event_id":
|
||||
property["description"] = "transport 事件 ID"
|
||||
case "subscribe_id":
|
||||
property["description"] = "个人事件订阅 ID"
|
||||
}
|
||||
properties[name] = property
|
||||
}
|
||||
return map[string]any{
|
||||
"type": "object",
|
||||
"properties": properties,
|
||||
}
|
||||
}
|
||||
|
||||
func outputTypeForEvent(eventKey string) reflect.Type {
|
||||
switch {
|
||||
case isMessageReceiveEvent(eventKey):
|
||||
|
||||
@@ -258,8 +258,18 @@ func Catalog(category string, enabledOnly, includePending bool) []Definition {
|
||||
}
|
||||
|
||||
func BuildSchemaDocument(def Definition) SchemaDocument {
|
||||
return BuildSchemaDocumentForMode(def, false)
|
||||
}
|
||||
|
||||
func BuildSchemaDocumentForMode(def Definition, flatten bool) SchemaDocument {
|
||||
requiredParams := make([]string, 0, len(def.RequiredParams))
|
||||
requiredParams = append(requiredParams, def.RequiredParams...)
|
||||
jqRootPath := ".data | fromjson"
|
||||
schema := transportEnvelopeSchema(def.EventKey)
|
||||
if flatten {
|
||||
jqRootPath = "."
|
||||
schema = outputSchema(def.EventKey)
|
||||
}
|
||||
return SchemaDocument{
|
||||
EventKey: def.EventKey,
|
||||
DisplayName: def.DisplayName,
|
||||
@@ -268,8 +278,8 @@ func BuildSchemaDocument(def Definition) SchemaDocument {
|
||||
RuleType: def.RuleType,
|
||||
RequiredParams: requiredParams,
|
||||
Constraints: cloneParameterConstraints(def.Constraints),
|
||||
JQRootPath: ".",
|
||||
Schema: outputSchema(def.EventKey),
|
||||
JQRootPath: jqRootPath,
|
||||
Schema: schema,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -95,7 +95,7 @@ func TestDefinitionJSONHidesInternalSchemaIDs(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestSchemaDocumentsUseSingleJSONSchema(t *testing.T) {
|
||||
func TestSchemaDocumentsDefaultToTransportEnvelope(t *testing.T) {
|
||||
for _, eventKey := range []string{EventMention, EventSingleChat, EventInChat, EventFromUser} {
|
||||
t.Run(eventKey, func(t *testing.T) {
|
||||
def, ok := Lookup(eventKey)
|
||||
@@ -117,16 +117,16 @@ func TestSchemaDocumentsUseSingleJSONSchema(t *testing.T) {
|
||||
"required_params",
|
||||
"jq_root_path",
|
||||
"schema",
|
||||
"type",
|
||||
"seq",
|
||||
"event_id",
|
||||
"timestamp",
|
||||
"event_born_time",
|
||||
"event_type",
|
||||
"subscribe_id",
|
||||
"content",
|
||||
"sender",
|
||||
"sender_open_dingtalk_id",
|
||||
"conversation_id",
|
||||
"message_id",
|
||||
"create_time",
|
||||
"event_time",
|
||||
"source_id",
|
||||
"data",
|
||||
"headers",
|
||||
"received_at_unix_ms",
|
||||
} {
|
||||
if !strings.Contains(out, want) {
|
||||
t.Fatalf("schema for %s missing %q: %s", eventKey, want, out)
|
||||
@@ -144,7 +144,6 @@ func TestSchemaDocumentsUseSingleJSONSchema(t *testing.T) {
|
||||
"payload_schema",
|
||||
"output_schema",
|
||||
"data_json_path",
|
||||
"headers",
|
||||
"audit",
|
||||
"tenant",
|
||||
"subject",
|
||||
@@ -152,13 +151,18 @@ func TestSchemaDocumentsUseSingleJSONSchema(t *testing.T) {
|
||||
"msgIdMetaq",
|
||||
"at_users",
|
||||
"sender_user_id",
|
||||
"sender_open_dingtalk_id",
|
||||
"conversation_id",
|
||||
"message_id",
|
||||
"create_time",
|
||||
"event_time",
|
||||
} {
|
||||
if strings.Contains(out, leaked) {
|
||||
t.Fatalf("schema for %s leaked %q: %s", eventKey, leaked, out)
|
||||
}
|
||||
}
|
||||
if doc.JQRootPath != "." {
|
||||
t.Fatalf("jq_root_path = %q, want .", doc.JQRootPath)
|
||||
if doc.JQRootPath != ".data | fromjson" {
|
||||
t.Fatalf("jq_root_path = %q, want .data | fromjson", doc.JQRootPath)
|
||||
}
|
||||
if doc.RequiredParams == nil {
|
||||
t.Fatalf("required_params = nil, want empty slice")
|
||||
@@ -167,6 +171,38 @@ func TestSchemaDocumentsUseSingleJSONSchema(t *testing.T) {
|
||||
if !ok {
|
||||
t.Fatalf("schema.properties = %#v, want object", doc.Schema["properties"])
|
||||
}
|
||||
wantProperties := []string{
|
||||
"type", "seq", "event_id", "event_born_time", "event_corp_id",
|
||||
"event_type", "event_unified_app_id", "event_scope", "subscribe_id",
|
||||
"source_id", "rule_type", "data", "headers", "received_at_unix_ms",
|
||||
}
|
||||
if len(props) != len(wantProperties) {
|
||||
t.Fatalf("schema.properties = %#v, want exactly %d transport fields", props, len(wantProperties))
|
||||
}
|
||||
for _, name := range wantProperties {
|
||||
if _, ok := props[name].(map[string]any); !ok {
|
||||
t.Fatalf("schema.properties.%s = %#v, want object", name, props[name])
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestFlattenedSchemaDocumentsUseMessageDTO(t *testing.T) {
|
||||
for _, eventKey := range []string{EventMention, EventSingleChat, EventInChat, EventFromUser} {
|
||||
t.Run(eventKey, func(t *testing.T) {
|
||||
def, ok := Lookup(eventKey)
|
||||
if !ok {
|
||||
t.Fatalf("Lookup(%q) failed", eventKey)
|
||||
}
|
||||
doc := BuildSchemaDocumentForMode(def, true)
|
||||
if doc.JQRootPath != "." {
|
||||
t.Fatalf("jq_root_path = %q, want .", doc.JQRootPath)
|
||||
}
|
||||
props, ok := doc.Schema["properties"].(map[string]any)
|
||||
if !ok {
|
||||
t.Fatalf("schema.properties = %#v, want object", doc.Schema["properties"])
|
||||
}
|
||||
wantProperties := []string{
|
||||
"type", "event_id", "timestamp", "subscribe_id", "message_id",
|
||||
"conversation_id", "sender", "sender_open_dingtalk_id", "content",
|
||||
@@ -180,6 +216,11 @@ func TestSchemaDocumentsUseSingleJSONSchema(t *testing.T) {
|
||||
t.Fatalf("schema.properties.%s = %#v, want object", name, props[name])
|
||||
}
|
||||
}
|
||||
for _, transportField := range []string{"data", "headers", "seq", "event_type"} {
|
||||
if _, ok := props[transportField]; ok {
|
||||
t.Fatalf("flattened schema exposed transport field %q", transportField)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -286,7 +327,7 @@ func TestActionSchemaDocumentsMatchOutputDTOs(t *testing.T) {
|
||||
if !ok {
|
||||
t.Fatalf("Lookup(%q) failed", eventKey)
|
||||
}
|
||||
doc := BuildSchemaDocument(def)
|
||||
doc := BuildSchemaDocumentForMode(def, true)
|
||||
if doc.JQRootPath != "." {
|
||||
t.Fatalf("jq_root_path = %q, want .", doc.JQRootPath)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
# Product Command Handler Agent Guide
|
||||
|
||||
This file applies to `internal/helpers/`. Read the root `AGENTS.md`,
|
||||
[`CONTRIBUTING.md`](../../CONTRIBUTING.md), and
|
||||
[`docs/coding-agent-guide.md`](../../docs/coding-agent-guide.md) first.
|
||||
For package/file layout, read
|
||||
[`docs/helpers-structure-guide.md`](../../docs/helpers-structure-guide.md)
|
||||
before adding or moving product leaves.
|
||||
|
||||
## Scope and routing
|
||||
|
||||
`internal/helpers` owns product command construction and handler behavior. Find
|
||||
the owning product file and its closest tests before editing.
|
||||
|
||||
| Concern | Owning surface |
|
||||
|---|---|
|
||||
| Root/static command wiring or plugin loading | `internal/app` |
|
||||
| Product command flags and handler behavior | `internal/helpers` |
|
||||
| Helpers file layout / megafile splits | [`docs/helpers-structure-guide.md`](../../docs/helpers-structure-guide.md) |
|
||||
| Shared Cobra construction | `internal/cobracmd` |
|
||||
| Invocation and transport | `internal/executor`, `internal/transport` |
|
||||
| Structured failures and recovery hints | `internal/errors`, `internal/recovery` |
|
||||
| Output encoding and projections | `internal/output` |
|
||||
| Confirmation and dry-run guards | `internal/safety` and command runtime gates |
|
||||
| Agent identity/Schema/parameters | `internal/cli` and [`docs/schema-contributor-guide.md`](../../docs/schema-contributor-guide.md) |
|
||||
|
||||
Do not modify `internal/app` or shared layers merely to register an ordinary
|
||||
product leaf when the existing helper construction already owns it. Cross the
|
||||
package boundary only when the task changes that shared contract.
|
||||
|
||||
## File layout (hard rules)
|
||||
|
||||
- Stay in flat `package helpers`; do not add `helpers/{product}` subpackages by
|
||||
default.
|
||||
- Product root `{product}.go` owns `new{Product}Command()` and wires subgroups.
|
||||
- Put leaves in `{product}_{resource}.go` by CLI resource (see `sheet_*.go`).
|
||||
- Do not grow megafiles such as `chat.go` or `aitable.go`; extract or add the
|
||||
resource file instead.
|
||||
- Mechanical splits must be behavior-neutral and keep registration symbols
|
||||
stable. Details and size guides:
|
||||
[`docs/helpers-structure-guide.md`](../../docs/helpers-structure-guide.md).
|
||||
|
||||
## Command contract
|
||||
|
||||
- Confirm the current path and flags with `dws <path> --help` and the Cobra
|
||||
construction before changing behavior.
|
||||
- Keep stdout machine-readable business data. Send progress, warnings, and
|
||||
diagnostics through the established stderr/logging paths.
|
||||
- Preserve structured error category, stable exit behavior, cause, trace ID,
|
||||
and an actionable recovery hint. Do not replace a typed lower-layer failure
|
||||
with an unclassified message.
|
||||
- Treat paths, JSON payloads, filenames, and remote content as untrusted input;
|
||||
use existing validation and sanitization helpers.
|
||||
- Mutating or destructive commands must keep their preview/confirmation
|
||||
behavior aligned with runtime safety and published Schema metadata.
|
||||
- If flags, identity, parameters, selection, or safety metadata change, follow
|
||||
the scoped `internal/cli/AGENTS.md` and regenerate from reviewed inputs.
|
||||
|
||||
## Verification matrix
|
||||
|
||||
| Change | Required focused evidence |
|
||||
|---|---|
|
||||
| Handler bug or behavior | package regression test including the failure path |
|
||||
| Flags or request mapping | Cobra/help test plus dry-run request assertion when the command supports preview |
|
||||
| Output or error behavior | structured payload, stderr/stdout, and exit-code assertions |
|
||||
| Mutating behavior | preview/confirmation test; live execution only with explicit authorization and disposable data |
|
||||
| Shared helper refactor | affected product tests plus `go test ./internal/helpers/...` |
|
||||
| Public command surface | Schema/command gates from `internal/cli/AGENTS.md` |
|
||||
|
||||
Before handoff, run the narrow package tests, `make format-check`, and the
|
||||
admission checks selected by `docs/coding-agent-guide.md`. Do not claim a live
|
||||
round-trip when only dry-run or mocked transport was exercised.
|
||||
Executable
+7
@@ -0,0 +1,7 @@
|
||||
#!/bin/sh
|
||||
set -eu
|
||||
|
||||
ROOT="$(CDPATH= cd -- "$(dirname -- "$0")/../.." && pwd)"
|
||||
|
||||
cd "$ROOT"
|
||||
exec go run ./scripts/policy/coding-agent-harness -root "$ROOT" "$@"
|
||||
@@ -0,0 +1,460 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"errors"
|
||||
"flag"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"sort"
|
||||
"strings"
|
||||
)
|
||||
|
||||
var guideLineLimits = map[string]int{
|
||||
"AGENTS.md": 80,
|
||||
"internal/cli/AGENTS.md": 80,
|
||||
"internal/helpers/AGENTS.md": 100,
|
||||
"skills/AGENTS.md": 80,
|
||||
}
|
||||
|
||||
var markdownLinkPattern = regexp.MustCompile(`\[[^]]+\]\(([^)]+)\)`)
|
||||
|
||||
type fileContract struct {
|
||||
path string
|
||||
required []string
|
||||
}
|
||||
|
||||
type taskField struct {
|
||||
label string
|
||||
allowNone bool
|
||||
allowedValue map[string]bool
|
||||
}
|
||||
|
||||
var taskFields = []taskField{
|
||||
{label: "Task kind", allowedValue: map[string]bool{"bug": true, "feature": true, "refactor": true, "docs": true, "policy": true, "release": true}},
|
||||
{label: "Goal (one primary outcome)"},
|
||||
{label: "Current behavior and evidence"},
|
||||
{label: "Acceptance criteria"},
|
||||
{label: "In scope (packages/files/surfaces)"},
|
||||
{label: "Out of scope", allowNone: true},
|
||||
{label: "Compatibility constraints", allowNone: true},
|
||||
{label: "Interface impact (commands/flags/output/errors/exit codes/Schema)", allowNone: true},
|
||||
{label: "Safety or data-mutation constraints", allowNone: true},
|
||||
{label: "Expected validation"},
|
||||
{label: "Known environment limitations", allowNone: true},
|
||||
}
|
||||
|
||||
var guideContracts = []fileContract{
|
||||
{
|
||||
path: "AGENTS.md",
|
||||
required: []string{
|
||||
"docs/coding-agent-guide.md",
|
||||
"docs/coding-agent-task-template.md",
|
||||
"docs/schema-contributor-guide.md",
|
||||
"docs/helpers-structure-guide.md",
|
||||
"docs/architecture.md",
|
||||
"docs/skill-authoring-guide.md",
|
||||
"skills/AGENTS.md",
|
||||
"internal/helpers/AGENTS.md",
|
||||
"docs/automation.md",
|
||||
"docs/agent-code.md",
|
||||
"Do not depend on generated Wiki or CodeWiki content.",
|
||||
},
|
||||
},
|
||||
{
|
||||
path: "CONTRIBUTING.md",
|
||||
required: []string{
|
||||
"docs/coding-agent-guide.md",
|
||||
"docs/schema-contributor-guide.md",
|
||||
},
|
||||
},
|
||||
{
|
||||
path: "docs/coding-agent-guide.md",
|
||||
required: []string{
|
||||
"## 1. Normalize the task input",
|
||||
"coding-agent-task-template.md",
|
||||
"make coding-agent-task TASK=",
|
||||
"primary outcome per task",
|
||||
"helpers-structure-guide.md",
|
||||
"not wired into CI",
|
||||
"github.com/larksuite/cli/blob/",
|
||||
"github.com/WecomTeam/wecom-cli/blob/",
|
||||
"## 2. Establish the baseline",
|
||||
"## 3. Implement from authoritative inputs",
|
||||
"## 4. Select validation by change surface",
|
||||
"Documentation only",
|
||||
"Go implementation",
|
||||
"CLI paths or flags",
|
||||
"Schema registry, hints, or generators",
|
||||
"CI or test sharding",
|
||||
"Packaging or installers",
|
||||
"Authentication, transport, or OS-specific code",
|
||||
"## 5. Pre-handoff self-check",
|
||||
"Outcome: what is now true",
|
||||
"Validation: exact commands and results",
|
||||
},
|
||||
},
|
||||
{
|
||||
path: "docs/architecture.md",
|
||||
required: []string{
|
||||
"## Change Rules",
|
||||
"helpers-structure-guide.md",
|
||||
"skill-authoring-guide.md",
|
||||
"schema-contributor-guide.md",
|
||||
"## Repository Structure",
|
||||
"internal/helpers",
|
||||
"skills/",
|
||||
},
|
||||
},
|
||||
{
|
||||
path: "docs/skill-authoring-guide.md",
|
||||
required: []string{
|
||||
"skills/mono/",
|
||||
"skills/multi/dingtalk-<product>/",
|
||||
"dws-shared",
|
||||
"Dual-write rule",
|
||||
"make skill-command-integrity",
|
||||
"schema-contributor-guide.md",
|
||||
},
|
||||
},
|
||||
{
|
||||
path: "skills/AGENTS.md",
|
||||
required: []string{
|
||||
"../docs/coding-agent-guide.md",
|
||||
"../docs/skill-authoring-guide.md",
|
||||
"../docs/schema-contributor-guide.md",
|
||||
"make skill-command-integrity",
|
||||
"Dual-write",
|
||||
},
|
||||
},
|
||||
{
|
||||
path: "docs/helpers-structure-guide.md",
|
||||
required: []string{
|
||||
"package helpers",
|
||||
"{product}.go",
|
||||
"{product}_{resource}.go",
|
||||
"sheet.go",
|
||||
"register_products.go",
|
||||
"Anti-pattern to stop growing",
|
||||
"Mechanical splits",
|
||||
"make coding-agent-harness",
|
||||
"not part of `make policy` or CI",
|
||||
},
|
||||
},
|
||||
{
|
||||
path: "docs/coding-agent-task-template.md",
|
||||
required: []string{
|
||||
"Goal (one primary outcome):",
|
||||
"Current behavior and evidence:",
|
||||
"Acceptance criteria:",
|
||||
"In scope (packages/files/surfaces):",
|
||||
"Out of scope:",
|
||||
"Compatibility constraints:",
|
||||
"Interface impact (commands/flags/output/errors/exit codes/Schema):",
|
||||
"Safety or data-mutation constraints:",
|
||||
"Expected validation:",
|
||||
"Known environment limitations:",
|
||||
"Expected stdout/stderr and exit behavior:",
|
||||
"Mutation preview/confirmation behavior:",
|
||||
},
|
||||
},
|
||||
{
|
||||
path: "docs/schema-contributor-guide.md",
|
||||
required: []string{
|
||||
"internal/cli/schema_command_registry.json",
|
||||
"internal/cli/schema_command_registry.schema.json",
|
||||
"internal/cli/schema_hints/metadata/<product>.json",
|
||||
"internal/cli/schema_hints/selection/<product>.json",
|
||||
"internal/cli/schema_parameter_bindings.json",
|
||||
"internal/cli/schema_mcp_metadata.json",
|
||||
"internal/cli/schema_command_exclusions.json",
|
||||
"internal/cli/schema_catalog.json",
|
||||
"make generate-schema",
|
||||
"make test-schema-agent-examples",
|
||||
},
|
||||
},
|
||||
{
|
||||
path: "internal/helpers/AGENTS.md",
|
||||
required: []string{
|
||||
"../../CONTRIBUTING.md",
|
||||
"../../docs/coding-agent-guide.md",
|
||||
"../../docs/schema-contributor-guide.md",
|
||||
"../../docs/helpers-structure-guide.md",
|
||||
"File layout (hard rules)",
|
||||
"Do not grow megafiles",
|
||||
"Keep stdout machine-readable business data.",
|
||||
"structured error category",
|
||||
"preview/confirmation",
|
||||
"Do not claim a live",
|
||||
},
|
||||
},
|
||||
{
|
||||
path: "internal/cli/AGENTS.md",
|
||||
required: []string{
|
||||
"../../docs/schema-contributor-guide.md",
|
||||
"schema_command_registry.json",
|
||||
"schema_parameter_bindings.json",
|
||||
"schema_hints/metadata/<product>.json",
|
||||
"schema_hints/selection/<product>.json",
|
||||
"schema_command_exclusions.json",
|
||||
"schema_catalog.json",
|
||||
"make generate-schema",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
var requiredPaths = []string{
|
||||
"docs/automation.md",
|
||||
"docs/agent-code.md",
|
||||
"scripts/policy/check-runtime-confirmation-truth.sh",
|
||||
"scripts/policy/check-coding-agent-harness.sh",
|
||||
"scripts/policy/check-generated-drift.sh",
|
||||
"scripts/policy/check-schema-catalog.sh",
|
||||
"scripts/policy/check-command-surface.sh",
|
||||
"scripts/release/verify-package-managers.sh",
|
||||
}
|
||||
|
||||
var requiredMakeTargets = []string{
|
||||
"build",
|
||||
"coding-agent-harness",
|
||||
"coding-agent-task",
|
||||
"format-check",
|
||||
"test",
|
||||
"policy",
|
||||
"interface-integrity",
|
||||
"skill-command-integrity",
|
||||
"test-schema-agent-examples",
|
||||
"generate-schema",
|
||||
"package",
|
||||
}
|
||||
|
||||
func main() {
|
||||
root := flag.String("root", ".", "repository root")
|
||||
task := flag.String("task", "", "optional filled coding-agent task file to validate")
|
||||
flag.Parse()
|
||||
if flag.NArg() != 0 {
|
||||
fmt.Fprintln(os.Stderr, "coding agent harness: unexpected positional arguments")
|
||||
os.Exit(2)
|
||||
}
|
||||
|
||||
problems := validate(*root)
|
||||
if strings.TrimSpace(*task) != "" {
|
||||
taskPath := *task
|
||||
if !filepath.IsAbs(taskPath) {
|
||||
taskPath = filepath.Join(*root, taskPath)
|
||||
}
|
||||
problems = append(problems, validateTask(taskPath)...)
|
||||
sort.Slice(problems, func(i, j int) bool { return problems[i].Error() < problems[j].Error() })
|
||||
}
|
||||
if len(problems) != 0 {
|
||||
fmt.Fprintln(os.Stderr, "coding agent harness: failed")
|
||||
for _, problem := range problems {
|
||||
fmt.Fprintf(os.Stderr, "- %v\n", problem)
|
||||
}
|
||||
os.Exit(1)
|
||||
}
|
||||
fmt.Println("coding agent harness: ok")
|
||||
}
|
||||
|
||||
func validateTask(path string) []error {
|
||||
content, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
return []error{fmt.Errorf("read task contract %s: %w", path, err)}
|
||||
}
|
||||
|
||||
values := make(map[string][]string)
|
||||
occurrences := make(map[string]int)
|
||||
current := ""
|
||||
scanner := bufio.NewScanner(strings.NewReader(string(content)))
|
||||
for scanner.Scan() {
|
||||
line := strings.TrimSpace(scanner.Text())
|
||||
matched := false
|
||||
for _, field := range taskFields {
|
||||
prefix := field.label + ":"
|
||||
if strings.HasPrefix(line, prefix) {
|
||||
current = field.label
|
||||
occurrences[current]++
|
||||
inline := strings.TrimSpace(strings.TrimPrefix(line, prefix))
|
||||
if inline != "" {
|
||||
values[current] = append(values[current], inline)
|
||||
}
|
||||
matched = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if matched || current == "" || line == "" || strings.HasPrefix(line, "```") {
|
||||
continue
|
||||
}
|
||||
values[current] = append(values[current], line)
|
||||
}
|
||||
if err := scanner.Err(); err != nil {
|
||||
return []error{fmt.Errorf("scan task contract %s: %w", path, err)}
|
||||
}
|
||||
|
||||
var problems []error
|
||||
for _, field := range taskFields {
|
||||
if occurrences[field.label] > 1 {
|
||||
problems = append(problems, fmt.Errorf("task contract field %q appears %d times; each field must be unique", field.label, occurrences[field.label]))
|
||||
}
|
||||
value := strings.TrimSpace(strings.Join(values[field.label], "\n"))
|
||||
if value == "" {
|
||||
problems = append(problems, fmt.Errorf("task contract is missing a value for %q", field.label))
|
||||
continue
|
||||
}
|
||||
normalized := strings.ToLower(value)
|
||||
if len(field.allowedValue) != 0 && !field.allowedValue[normalized] {
|
||||
problems = append(problems, fmt.Errorf("task contract field %q has unsupported value %q", field.label, value))
|
||||
}
|
||||
if isPlaceholderTaskValue(normalized) {
|
||||
problems = append(problems, fmt.Errorf("task contract field %q still contains placeholder value %q", field.label, value))
|
||||
}
|
||||
if !field.allowNone && (normalized == "none" || normalized == "n/a" || normalized == "not applicable") {
|
||||
problems = append(problems, fmt.Errorf("task contract field %q requires concrete evidence", field.label))
|
||||
}
|
||||
}
|
||||
sort.Slice(problems, func(i, j int) bool { return problems[i].Error() < problems[j].Error() })
|
||||
return problems
|
||||
}
|
||||
|
||||
func isPlaceholderTaskValue(value string) bool {
|
||||
for _, line := range strings.Split(value, "\n") {
|
||||
trimmed := strings.TrimSpace(line)
|
||||
trimmed = strings.TrimLeft(trimmed, "-*0123456789.) ")
|
||||
switch trimmed {
|
||||
case "todo", "tbd", "unknown", "fill me", "to be decided":
|
||||
return true
|
||||
}
|
||||
if strings.HasPrefix(trimmed, "todo:") || strings.HasPrefix(trimmed, "tbd:") || strings.Contains(trimmed, "<fill") || strings.Contains(trimmed, "<todo") {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func validate(root string) []error {
|
||||
absRoot, err := filepath.Abs(root)
|
||||
if err != nil {
|
||||
return []error{fmt.Errorf("resolve root: %w", err)}
|
||||
}
|
||||
|
||||
var problems []error
|
||||
for _, contract := range guideContracts {
|
||||
content, readErr := os.ReadFile(filepath.Join(absRoot, filepath.FromSlash(contract.path)))
|
||||
if readErr != nil {
|
||||
problems = append(problems, fmt.Errorf("read %s: %w", contract.path, readErr))
|
||||
continue
|
||||
}
|
||||
text := string(content)
|
||||
for _, required := range contract.required {
|
||||
if !strings.Contains(text, required) {
|
||||
problems = append(problems, fmt.Errorf("%s is missing required contract text %q", contract.path, required))
|
||||
}
|
||||
}
|
||||
problems = append(problems, validateLocalLinks(absRoot, contract.path, text)...)
|
||||
}
|
||||
|
||||
for path, limit := range guideLineLimits {
|
||||
if lineCount, countErr := countLines(filepath.Join(absRoot, filepath.FromSlash(path))); countErr != nil {
|
||||
problems = append(problems, fmt.Errorf("count %s lines: %w", path, countErr))
|
||||
} else if lineCount > limit {
|
||||
problems = append(problems, fmt.Errorf("%s has %d lines; scoped guide limit is %d", path, lineCount, limit))
|
||||
}
|
||||
}
|
||||
|
||||
for _, path := range requiredPaths {
|
||||
info, statErr := os.Stat(filepath.Join(absRoot, filepath.FromSlash(path)))
|
||||
if statErr != nil {
|
||||
problems = append(problems, fmt.Errorf("required referenced path %s: %w", path, statErr))
|
||||
} else if strings.HasSuffix(path, ".sh") && info.Mode().Perm()&0o111 == 0 {
|
||||
problems = append(problems, fmt.Errorf("required referenced script %s is not executable", path))
|
||||
}
|
||||
}
|
||||
|
||||
makeTargets, makeErr := loadMakeTargets(filepath.Join(absRoot, "Makefile"))
|
||||
if makeErr != nil {
|
||||
problems = append(problems, makeErr)
|
||||
} else {
|
||||
for _, target := range requiredMakeTargets {
|
||||
if !makeTargets[target] {
|
||||
problems = append(problems, fmt.Errorf("makefile is missing referenced target %q", target))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
sort.Slice(problems, func(i, j int) bool { return problems[i].Error() < problems[j].Error() })
|
||||
return problems
|
||||
}
|
||||
|
||||
func validateLocalLinks(root, document, content string) []error {
|
||||
var problems []error
|
||||
documentDir := filepath.Join(root, filepath.Dir(filepath.FromSlash(document)))
|
||||
for _, match := range markdownLinkPattern.FindAllStringSubmatch(content, -1) {
|
||||
target := strings.TrimSpace(match[1])
|
||||
if target == "" || strings.HasPrefix(target, "#") || strings.HasPrefix(target, "http://") || strings.HasPrefix(target, "https://") || strings.HasPrefix(target, "mailto:") {
|
||||
continue
|
||||
}
|
||||
if cut, _, ok := strings.Cut(target, "#"); ok {
|
||||
target = cut
|
||||
}
|
||||
if target == "" {
|
||||
continue
|
||||
}
|
||||
if filepath.IsAbs(target) {
|
||||
problems = append(problems, fmt.Errorf("%s contains absolute local link %q", document, match[1]))
|
||||
continue
|
||||
}
|
||||
resolved := filepath.Clean(filepath.Join(documentDir, filepath.FromSlash(target)))
|
||||
rel, err := filepath.Rel(root, resolved)
|
||||
if err != nil || rel == ".." || strings.HasPrefix(rel, ".."+string(filepath.Separator)) {
|
||||
problems = append(problems, fmt.Errorf("%s link %q escapes repository root", document, match[1]))
|
||||
continue
|
||||
}
|
||||
if _, err := os.Stat(resolved); err != nil {
|
||||
problems = append(problems, fmt.Errorf("%s has broken local link %q: %w", document, match[1], err))
|
||||
}
|
||||
}
|
||||
return problems
|
||||
}
|
||||
|
||||
func countLines(path string) (int, error) {
|
||||
file, err := os.Open(path)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
scanner := bufio.NewScanner(file)
|
||||
count := 0
|
||||
for scanner.Scan() {
|
||||
count++
|
||||
}
|
||||
return count, scanner.Err()
|
||||
}
|
||||
|
||||
func loadMakeTargets(path string) (map[string]bool, error) {
|
||||
file, err := os.Open(path)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("read Makefile targets: %w", err)
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
targets := make(map[string]bool)
|
||||
scanner := bufio.NewScanner(file)
|
||||
for scanner.Scan() {
|
||||
line := scanner.Text()
|
||||
if line == "" || line[0] == '\t' || strings.HasPrefix(strings.TrimSpace(line), "#") {
|
||||
continue
|
||||
}
|
||||
name, _, ok := strings.Cut(line, ":")
|
||||
if !ok || strings.ContainsAny(name, "= ") || strings.HasPrefix(name, ".") {
|
||||
continue
|
||||
}
|
||||
targets[name] = true
|
||||
}
|
||||
if err := scanner.Err(); err != nil {
|
||||
return nil, errors.New("scan Makefile targets: " + err.Error())
|
||||
}
|
||||
return targets, nil
|
||||
}
|
||||
@@ -0,0 +1,250 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestRepositoryCodingAgentHarness(t *testing.T) {
|
||||
root, err := filepath.Abs(filepath.Join("..", "..", ".."))
|
||||
if err != nil {
|
||||
t.Fatalf("resolve repository root: %v", err)
|
||||
}
|
||||
if problems := validate(root); len(problems) != 0 {
|
||||
t.Fatalf("repository harness problems:\n%s", formatProblems(problems))
|
||||
}
|
||||
}
|
||||
|
||||
func TestCodingAgentHarnessFixturePasses(t *testing.T) {
|
||||
root := newHarnessFixture(t)
|
||||
if problems := validate(root); len(problems) != 0 {
|
||||
t.Fatalf("valid fixture problems:\n%s", formatProblems(problems))
|
||||
}
|
||||
}
|
||||
|
||||
func TestCodingAgentTaskContractPasses(t *testing.T) {
|
||||
path := filepath.Join(t.TempDir(), "task.md")
|
||||
writeTaskFixture(t, path, validTaskContract)
|
||||
if problems := validateTask(path); len(problems) != 0 {
|
||||
t.Fatalf("valid task problems:\n%s", formatProblems(problems))
|
||||
}
|
||||
}
|
||||
|
||||
func TestCodingAgentTaskContractFailsClosed(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
content string
|
||||
wantProblem string
|
||||
}{
|
||||
{
|
||||
name: "missing goal",
|
||||
content: strings.Replace(validTaskContract, "Goal (one primary outcome): Fix retry handling\n", "", 1),
|
||||
wantProblem: `missing a value for "Goal (one primary outcome)"`,
|
||||
},
|
||||
{
|
||||
name: "blank acceptance criteria",
|
||||
content: strings.Replace(validTaskContract, "Acceptance criteria:\n- The retry succeeds once and then stops.\n", "Acceptance criteria:\n", 1),
|
||||
wantProblem: `missing a value for "Acceptance criteria"`,
|
||||
},
|
||||
{
|
||||
name: "unsupported task kind",
|
||||
content: strings.Replace(validTaskContract, "Task kind: bug", "Task kind: experiment", 1),
|
||||
wantProblem: `unsupported value "experiment"`,
|
||||
},
|
||||
{
|
||||
name: "placeholder acceptance bullet",
|
||||
content: strings.Replace(validTaskContract, "- The retry succeeds once and then stops.", "- TBD: decide expected retry result", 1),
|
||||
wantProblem: "still contains placeholder value",
|
||||
},
|
||||
{
|
||||
name: "duplicate goal",
|
||||
content: strings.Replace(validTaskContract, "Goal (one primary outcome): Fix retry handling", "Goal (one primary outcome): Fix retry handling\nGoal (one primary outcome): Refactor transport", 1),
|
||||
wantProblem: `field "Goal (one primary outcome)" appears 2 times`,
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
path := filepath.Join(t.TempDir(), "task.md")
|
||||
writeTaskFixture(t, path, test.content)
|
||||
problems := formatProblems(validateTask(path))
|
||||
if !strings.Contains(problems, test.wantProblem) {
|
||||
t.Fatalf("problems:\n%s\nwant substring %q", problems, test.wantProblem)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestCodingAgentHarnessFailsClosed(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
mutate func(t *testing.T, root string)
|
||||
wantProblem string
|
||||
}{
|
||||
{
|
||||
name: "root guide is no longer thin",
|
||||
mutate: func(t *testing.T, root string) {
|
||||
path := filepath.Join(root, "AGENTS.md")
|
||||
file, err := os.OpenFile(path, os.O_APPEND|os.O_WRONLY, 0)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer file.Close()
|
||||
for i := 0; i < guideLineLimits["AGENTS.md"]; i++ {
|
||||
if _, err := file.WriteString("extra line\n"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
},
|
||||
wantProblem: "scoped guide limit",
|
||||
},
|
||||
{
|
||||
name: "task input field is removed",
|
||||
mutate: func(t *testing.T, root string) {
|
||||
replaceFixtureText(t, root, "docs/coding-agent-task-template.md", "Acceptance criteria:", "")
|
||||
},
|
||||
wantProblem: `docs/coding-agent-task-template.md is missing required contract text "Acceptance criteria:"`,
|
||||
},
|
||||
{
|
||||
name: "route link is broken",
|
||||
mutate: func(t *testing.T, root string) {
|
||||
replaceFixtureText(t, root, "AGENTS.md", "[automation](docs/automation.md)", "[automation](docs/missing.md)")
|
||||
},
|
||||
wantProblem: "broken local link",
|
||||
},
|
||||
{
|
||||
name: "referenced script is absent",
|
||||
mutate: func(t *testing.T, root string) {
|
||||
if err := os.Remove(filepath.Join(root, "scripts/policy/check-schema-catalog.sh")); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
},
|
||||
wantProblem: "required referenced path scripts/policy/check-schema-catalog.sh",
|
||||
},
|
||||
{
|
||||
name: "make target is absent",
|
||||
mutate: func(t *testing.T, root string) {
|
||||
replaceFixtureText(t, root, "Makefile", "package:\n", "removed-package:\n")
|
||||
},
|
||||
wantProblem: `makefile is missing referenced target "package"`,
|
||||
},
|
||||
{
|
||||
name: "harness script is not executable",
|
||||
mutate: func(t *testing.T, root string) {
|
||||
path := filepath.Join(root, "scripts/policy/check-coding-agent-harness.sh")
|
||||
if err := os.Chmod(path, 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
},
|
||||
wantProblem: "required referenced script scripts/policy/check-coding-agent-harness.sh is not executable",
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
root := newHarnessFixture(t)
|
||||
test.mutate(t, root)
|
||||
problems := formatProblems(validate(root))
|
||||
if !strings.Contains(problems, test.wantProblem) {
|
||||
t.Fatalf("problems:\n%s\nwant substring %q", problems, test.wantProblem)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func newHarnessFixture(t *testing.T) string {
|
||||
t.Helper()
|
||||
root := t.TempDir()
|
||||
|
||||
for _, contract := range guideContracts {
|
||||
var content strings.Builder
|
||||
content.WriteString("# Fixture\n\n")
|
||||
for _, required := range contract.required {
|
||||
content.WriteString(required)
|
||||
content.WriteByte('\n')
|
||||
}
|
||||
if contract.path == "AGENTS.md" {
|
||||
content.WriteString("[coding](docs/coding-agent-guide.md)\n")
|
||||
content.WriteString("[schema](docs/schema-contributor-guide.md)\n")
|
||||
content.WriteString("[automation](docs/automation.md)\n")
|
||||
content.WriteString("[agent code](docs/agent-code.md)\n")
|
||||
}
|
||||
writeFixtureFile(t, root, contract.path, content.String())
|
||||
}
|
||||
|
||||
for _, path := range requiredPaths {
|
||||
writeFixtureFileMode(t, root, path, "fixture\n", 0o755)
|
||||
}
|
||||
var makefile strings.Builder
|
||||
for _, target := range requiredMakeTargets {
|
||||
makefile.WriteString(target)
|
||||
makefile.WriteString(":\n\t@true\n")
|
||||
}
|
||||
writeFixtureFile(t, root, "Makefile", makefile.String())
|
||||
return root
|
||||
}
|
||||
|
||||
func writeFixtureFile(t *testing.T, root, path, content string) {
|
||||
t.Helper()
|
||||
writeFixtureFileMode(t, root, path, content, 0o644)
|
||||
}
|
||||
|
||||
func writeFixtureFileMode(t *testing.T, root, path, content string, mode os.FileMode) {
|
||||
t.Helper()
|
||||
full := filepath.Join(root, filepath.FromSlash(path))
|
||||
if err := os.MkdirAll(filepath.Dir(full), 0o755); err != nil {
|
||||
t.Fatalf("create fixture directory: %v", err)
|
||||
}
|
||||
if err := os.WriteFile(full, []byte(content), mode); err != nil {
|
||||
t.Fatalf("write fixture %s: %v", path, err)
|
||||
}
|
||||
}
|
||||
|
||||
func replaceFixtureText(t *testing.T, root, path, old, replacement string) {
|
||||
t.Helper()
|
||||
full := filepath.Join(root, filepath.FromSlash(path))
|
||||
content, err := os.ReadFile(full)
|
||||
if err != nil {
|
||||
t.Fatalf("read fixture %s: %v", path, err)
|
||||
}
|
||||
updated := strings.Replace(string(content), old, replacement, 1)
|
||||
if updated == string(content) {
|
||||
t.Fatalf("fixture %s does not contain %q", path, old)
|
||||
}
|
||||
if err := os.WriteFile(full, []byte(updated), 0o644); err != nil {
|
||||
t.Fatalf("update fixture %s: %v", path, err)
|
||||
}
|
||||
}
|
||||
|
||||
func formatProblems(problems []error) string {
|
||||
var lines []string
|
||||
for _, problem := range problems {
|
||||
lines = append(lines, problem.Error())
|
||||
}
|
||||
return strings.Join(lines, "\n")
|
||||
}
|
||||
|
||||
func writeTaskFixture(t *testing.T, path, content string) {
|
||||
t.Helper()
|
||||
if err := os.WriteFile(path, []byte(content), 0o644); err != nil {
|
||||
t.Fatalf("write task fixture: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
const validTaskContract = `# Task
|
||||
|
||||
Task kind: bug
|
||||
Goal (one primary outcome): Fix retry handling
|
||||
Current behavior and evidence: The focused test reproduces two retries.
|
||||
Acceptance criteria:
|
||||
- The retry succeeds once and then stops.
|
||||
In scope (packages/files/surfaces): internal/transport
|
||||
Out of scope: Authentication behavior
|
||||
Compatibility constraints: Preserve existing flags and output
|
||||
Interface impact (commands/flags/output/errors/exit codes/Schema): None
|
||||
Safety or data-mutation constraints: No external writes
|
||||
Expected validation: Focused unit test and make test
|
||||
Known environment limitations: Live service unavailable
|
||||
`
|
||||
@@ -0,0 +1,15 @@
|
||||
# Retry Handling Task
|
||||
|
||||
Task kind: bug
|
||||
Goal (one primary outcome): Fix transient retry handling
|
||||
Current behavior and evidence: A focused transport test reproduces two retries after a successful response.
|
||||
Acceptance criteria:
|
||||
- The retry loop stops after the first successful response.
|
||||
- The existing output and exit behavior remain unchanged.
|
||||
In scope (packages/files/surfaces): internal/transport retry implementation and tests
|
||||
Out of scope: Authentication and endpoint discovery
|
||||
Compatibility constraints: Preserve existing flags, JSON output, and exit codes
|
||||
Interface impact (commands/flags/output/errors/exit codes/Schema): None
|
||||
Safety or data-mutation constraints: No live service calls or external writes
|
||||
Expected validation: Focused transport tests, race test, formatting, and full Go test suite
|
||||
Known environment limitations: Live DingTalk service is intentionally not used
|
||||
@@ -8,16 +8,32 @@ GITEE_API="${GITEE_API:-https://gitee.com/api/v5}"
|
||||
GITEE_CURL_CONNECT_TIMEOUT="${GITEE_CURL_CONNECT_TIMEOUT:-15}"
|
||||
GITEE_CURL_MAX_TIME="${GITEE_CURL_MAX_TIME:-120}"
|
||||
GITEE_LIST_MAX_TIME="${GITEE_LIST_MAX_TIME:-20}"
|
||||
# Attachment-list reads are safe to retry and are used both before an upload
|
||||
# and to detect a committed upload whose HTTP response was lost. Keep retrying
|
||||
# long enough to bridge a short Gitee TLS/API outage without ever blindly
|
||||
# replaying the upload itself.
|
||||
GITEE_LIST_RETRIES="${GITEE_LIST_RETRIES:-24}"
|
||||
GITEE_LIST_RETRY_DELAY="${GITEE_LIST_RETRY_DELAY:-20}"
|
||||
GITEE_LIST_RETRY_WINDOW_SECONDS="${GITEE_LIST_RETRY_WINDOW_SECONDS:-420}"
|
||||
GITEE_VERIFY_MAX_TIME="${GITEE_VERIFY_MAX_TIME:-60}"
|
||||
GITEE_MUTATION_MAX_TIME="${GITEE_MUTATION_MAX_TIME:-20}"
|
||||
GITEE_UPLOAD_MAX_TIME="${GITEE_UPLOAD_MAX_TIME:-120}"
|
||||
# Gitee does not return a response until an attachment upload is committed.
|
||||
# From GitHub-hosted runners, a near-10 MiB DWS binary can legitimately take
|
||||
# more than five minutes, so keep the transfer deadline above that observed
|
||||
# floor while the per-asset and overall deadlines retain hard upper bounds.
|
||||
GITEE_UPLOAD_MAX_TIME="${GITEE_UPLOAD_MAX_TIME:-1200}"
|
||||
# The per-asset deadline guarantees one complete slow upload plus one complete
|
||||
# attachment-list recovery on each side of that upload. The second upload
|
||||
# attempt is allowed only for zero-byte failures before HTTP begins; a first
|
||||
# attempt that consumes the full transfer deadline intentionally exhausts the
|
||||
# retry budget instead of holding a runner for another full slow upload.
|
||||
GITEE_UPLOAD_RETRIES="${GITEE_UPLOAD_RETRIES:-2}"
|
||||
GITEE_UPLOAD_RETRY_DELAY="${GITEE_UPLOAD_RETRY_DELAY:-5}"
|
||||
GITEE_EXISTING_VERIFY_ATTEMPTS="${GITEE_EXISTING_VERIFY_ATTEMPTS:-1}"
|
||||
GITEE_POST_UPLOAD_VERIFY_ATTEMPTS="${GITEE_POST_UPLOAD_VERIFY_ATTEMPTS:-2}"
|
||||
GITEE_VERIFY_RETRY_DELAY="${GITEE_VERIFY_RETRY_DELAY:-5}"
|
||||
GITEE_ASSET_TIMEOUT_SECONDS="${GITEE_ASSET_TIMEOUT_SECONDS:-600}"
|
||||
GITEE_OVERALL_TIMEOUT_SECONDS="${GITEE_OVERALL_TIMEOUT_SECONDS:-4920}"
|
||||
GITEE_ASSET_TIMEOUT_SECONDS="${GITEE_ASSET_TIMEOUT_SECONDS:-2220}"
|
||||
GITEE_OVERALL_TIMEOUT_SECONDS="${GITEE_OVERALL_TIMEOUT_SECONDS:-18300}"
|
||||
|
||||
err() {
|
||||
printf 'error: %s\n' "$*" >&2
|
||||
@@ -43,6 +59,8 @@ for setting in \
|
||||
GITEE_CURL_CONNECT_TIMEOUT \
|
||||
GITEE_CURL_MAX_TIME \
|
||||
GITEE_LIST_MAX_TIME \
|
||||
GITEE_LIST_RETRIES \
|
||||
GITEE_LIST_RETRY_WINDOW_SECONDS \
|
||||
GITEE_VERIFY_MAX_TIME \
|
||||
GITEE_MUTATION_MAX_TIME \
|
||||
GITEE_UPLOAD_MAX_TIME \
|
||||
@@ -54,6 +72,7 @@ for setting in \
|
||||
require_positive_integer "$setting" "${!setting}"
|
||||
done
|
||||
for setting in \
|
||||
GITEE_LIST_RETRY_DELAY \
|
||||
GITEE_UPLOAD_RETRY_DELAY \
|
||||
GITEE_VERIFY_RETRY_DELAY; do
|
||||
require_nonnegative_integer "$setting" "${!setting}"
|
||||
@@ -139,21 +158,81 @@ api_get() {
|
||||
--max-time "$max_time" "$@"
|
||||
}
|
||||
|
||||
list_assets() {
|
||||
api_get "$GITEE_LIST_MAX_TIME" \
|
||||
list_assets_once() {
|
||||
local max_time="$1" payload
|
||||
payload="$(api_get "$max_time" \
|
||||
-H "Authorization: token ${GITEE_TOKEN}" \
|
||||
"${base}/releases/${release_id}/attach_files" \
|
||||
| python3 -c 'import json,sys
|
||||
"${base}/releases/${release_id}/attach_files")" || return 1
|
||||
printf '%s\n' "$payload" | python3 -c 'import json,sys
|
||||
data=json.load(sys.stdin)
|
||||
rows=data if isinstance(data,list) else data.get("attach_files",[])
|
||||
if isinstance(data,list):
|
||||
rows=data
|
||||
elif isinstance(data,dict) and "attach_files" in data:
|
||||
rows=data["attach_files"]
|
||||
else:
|
||||
raise ValueError("attachment list response must be a list or contain attach_files")
|
||||
if not isinstance(rows,list):
|
||||
raise ValueError("attach_files must be a list")
|
||||
for asset in rows:
|
||||
if not isinstance(asset,dict):
|
||||
raise ValueError("each attachment must be an object")
|
||||
name=asset.get("name","")
|
||||
asset_id=asset.get("id","")
|
||||
url=asset.get("browser_download_url","")
|
||||
if name and asset_id != "":
|
||||
print("%s\t%s\t%s" % (name, asset_id, url))'
|
||||
valid_id=(isinstance(asset_id,int) and not isinstance(asset_id,bool) and asset_id > 0) or (isinstance(asset_id,str) and asset_id.isdigit() and int(asset_id) > 0)
|
||||
if not isinstance(name,str) or not name or not valid_id or not isinstance(url,str) or not url:
|
||||
raise ValueError("attachment identity and download URL are required")
|
||||
print("%s\t%s\t%s" % (name, asset_id, url))'
|
||||
}
|
||||
|
||||
list_assets() {
|
||||
local attempt=1 candidate now retry_deadline remaining max_time delay
|
||||
now="$(now_seconds)"
|
||||
retry_deadline=$(( now + GITEE_LIST_RETRY_WINDOW_SECONDS ))
|
||||
if [ "$retry_deadline" -gt "$active_deadline" ]; then
|
||||
retry_deadline="$active_deadline"
|
||||
fi
|
||||
while [ "$attempt" -le "$GITEE_LIST_RETRIES" ]; do
|
||||
now="$(now_seconds)"
|
||||
remaining=$(( retry_deadline - now ))
|
||||
[ "$remaining" -gt 0 ] || break
|
||||
max_time="$GITEE_LIST_MAX_TIME"
|
||||
if [ "$max_time" -gt "$remaining" ]; then
|
||||
max_time="$remaining"
|
||||
fi
|
||||
# Publish one complete parse atomically. A malformed response can make the
|
||||
# parser emit valid leading rows before it fails; leaking those rows into a
|
||||
# later successful retry would manufacture duplicates and could delete a
|
||||
# correct attachment.
|
||||
if candidate="$(list_assets_once "$max_time")"; then
|
||||
now="$(now_seconds)"
|
||||
if [ "$now" -lt "$retry_deadline" ]; then
|
||||
printf '%s\n' "$candidate"
|
||||
return 0
|
||||
fi
|
||||
break
|
||||
fi
|
||||
if [ "$attempt" -ge "$GITEE_LIST_RETRIES" ]; then
|
||||
break
|
||||
fi
|
||||
attempt=$((attempt + 1))
|
||||
now="$(now_seconds)"
|
||||
remaining=$(( retry_deadline - now ))
|
||||
[ "$remaining" -gt 0 ] || break
|
||||
delay="$GITEE_LIST_RETRY_DELAY"
|
||||
if [ "$delay" -gt 0 ]; then
|
||||
# Do not turn a configured backoff into a burst of zero-delay requests
|
||||
# during the final wall-clock second. Explicit delay=0 remains available
|
||||
# to fast unit tests and tightly controlled callers.
|
||||
[ "$remaining" -gt 1 ] || break
|
||||
if [ "$delay" -ge "$remaining" ]; then
|
||||
delay=$(( remaining - 1 ))
|
||||
fi
|
||||
fi
|
||||
echo " ⚠ Gitee attachment list attempt $((attempt - 1))/${GITEE_LIST_RETRIES} failed; retrying in ${delay}s" >&2
|
||||
sleep_within_deadline "$delay" || return 1
|
||||
done
|
||||
return 1
|
||||
}
|
||||
|
||||
sha256_of() {
|
||||
@@ -234,20 +313,42 @@ delete_named_assets() {
|
||||
}
|
||||
|
||||
gitee_attach() {
|
||||
local file="$1" name attempt response status max_time
|
||||
local file="$1" name attempt response status max_time transfer_log metrics http_code size_upload safe_to_retry
|
||||
name="$(basename "$file")"
|
||||
attempt=1
|
||||
while [ "$attempt" -le "$GITEE_UPLOAD_RETRIES" ]; do
|
||||
status=0
|
||||
max_time="$(bounded_max_time "$GITEE_UPLOAD_MAX_TIME")" || return 1
|
||||
if response="$(curl -fsS --connect-timeout "$GITEE_CURL_CONNECT_TIMEOUT" \
|
||||
transfer_log="$(mktemp)" || return 1
|
||||
if metrics="$(curl -fsS --connect-timeout "$GITEE_CURL_CONNECT_TIMEOUT" \
|
||||
--max-time "$max_time" \
|
||||
-X POST "${base}/releases/${release_id}/attach_files" \
|
||||
-H "Authorization: token ${GITEE_TOKEN}" -F "file=@${file}" 2>&1)"; then
|
||||
-H "Authorization: token ${GITEE_TOKEN}" \
|
||||
-H "Expect:" \
|
||||
-F "file=@${file}" \
|
||||
-o /dev/null \
|
||||
-w '%{http_code}\t%{size_upload}' \
|
||||
2>"$transfer_log")"; then
|
||||
status=0
|
||||
else
|
||||
status=$?
|
||||
fi
|
||||
response="$(<"$transfer_log")"
|
||||
rm -f "$transfer_log"
|
||||
http_code="${metrics%%$'\t'*}"
|
||||
if [ "$metrics" = "$http_code" ]; then
|
||||
size_upload=""
|
||||
else
|
||||
size_upload="${metrics#*$'\t'}"
|
||||
fi
|
||||
safe_to_retry=0
|
||||
if [ "$status" -ne 0 ] && [ "$http_code" = "000" ] && \
|
||||
awk -v value="$size_upload" 'BEGIN { exit !((value + 0) == 0 && value ~ /^[0-9]+([.][0-9]+)?$/) }'; then
|
||||
# A request that never reached HTTP and uploaded zero bytes cannot have
|
||||
# committed an attachment. TLS/DNS/connect failures are therefore the
|
||||
# only transport failures safe to replay automatically.
|
||||
safe_to_retry=1
|
||||
fi
|
||||
|
||||
# Gitee can commit an upload but let the HTTP response time out. Probe the
|
||||
# release before retrying so a lost response does not create duplicates.
|
||||
@@ -259,6 +360,10 @@ gitee_attach() {
|
||||
fi
|
||||
|
||||
echo " ⚠ upload attempt ${attempt}/${GITEE_UPLOAD_RETRIES} failed for ${name}: $(printf '%s' "$response" | head -c 240)" >&2
|
||||
if [ "$safe_to_retry" -ne 1 ]; then
|
||||
echo " ⚠ ${name} upload outcome is ambiguous (HTTP ${http_code:-unknown}, uploaded ${size_upload:-unknown} bytes); refusing to replay POST" >&2
|
||||
return 1
|
||||
fi
|
||||
attempt=$((attempt + 1))
|
||||
if [ "$attempt" -le "$GITEE_UPLOAD_RETRIES" ]; then
|
||||
# Remove any partial, stale, or duplicate attachment before the one
|
||||
|
||||
@@ -31,11 +31,13 @@ DIST_DIR="${DIST_DIR:-dist}"
|
||||
GITEE_API="${GITEE_API:-https://gitee.com/api/v5}"
|
||||
GITEE_CURL_CONNECT_TIMEOUT="${GITEE_CURL_CONNECT_TIMEOUT:-15}"
|
||||
GITEE_CURL_MAX_TIME="${GITEE_CURL_MAX_TIME:-120}"
|
||||
GITEE_SYNC_TIMEOUT_SECONDS="${GITEE_SYNC_TIMEOUT_SECONDS:-5700}"
|
||||
GITEE_SYNC_TIMEOUT_SECONDS="${GITEE_SYNC_TIMEOUT_SECONDS:-18840}"
|
||||
GITEE_TAG_TIMEOUT_SECONDS="${GITEE_TAG_TIMEOUT_SECONDS:-300}"
|
||||
GITEE_RELEASE_LOOKUP_MAX_TIME="${GITEE_RELEASE_LOOKUP_MAX_TIME:-60}"
|
||||
GITEE_RELEASE_LOOKUP_RETRIES="${GITEE_RELEASE_LOOKUP_RETRIES:-2}"
|
||||
GITEE_RELEASE_LOOKUP_RETRY_DELAY="${GITEE_RELEASE_LOOKUP_RETRY_DELAY:-2}"
|
||||
GITEE_RELEASE_CREATE_MAX_TIME="${GITEE_RELEASE_CREATE_MAX_TIME:-60}"
|
||||
GITEE_RECONCILE_TIMEOUT_SECONDS="${GITEE_RECONCILE_TIMEOUT_SECONDS:-4920}"
|
||||
GITEE_RECONCILE_TIMEOUT_SECONDS="${GITEE_RECONCILE_TIMEOUT_SECONDS:-18300}"
|
||||
GITEE_CHILD_DEADLINE_RESERVE_SECONDS="${GITEE_CHILD_DEADLINE_RESERVE_SECONDS:-5}"
|
||||
|
||||
err() {
|
||||
@@ -51,17 +53,28 @@ require_positive_integer() {
|
||||
[ "$value" -gt 0 ] || err "${name} must be greater than zero"
|
||||
}
|
||||
|
||||
require_nonnegative_integer() {
|
||||
local name="$1" value="$2"
|
||||
case "$value" in
|
||||
''|*[!0-9]*) err "${name} must be a non-negative integer: ${value}" ;;
|
||||
esac
|
||||
}
|
||||
|
||||
for setting in \
|
||||
GITEE_CURL_CONNECT_TIMEOUT \
|
||||
GITEE_CURL_MAX_TIME \
|
||||
GITEE_SYNC_TIMEOUT_SECONDS \
|
||||
GITEE_TAG_TIMEOUT_SECONDS \
|
||||
GITEE_RELEASE_LOOKUP_MAX_TIME \
|
||||
GITEE_RELEASE_LOOKUP_RETRIES \
|
||||
GITEE_RELEASE_CREATE_MAX_TIME \
|
||||
GITEE_RECONCILE_TIMEOUT_SECONDS \
|
||||
GITEE_CHILD_DEADLINE_RESERVE_SECONDS; do
|
||||
require_positive_integer "$setting" "${!setting}"
|
||||
done
|
||||
require_nonnegative_integer \
|
||||
GITEE_RELEASE_LOOKUP_RETRY_DELAY \
|
||||
"$GITEE_RELEASE_LOOKUP_RETRY_DELAY"
|
||||
|
||||
missing=""
|
||||
[ -z "${GITEE_TOKEN:-}" ] && missing="$missing GITEE_TOKEN"
|
||||
@@ -117,14 +130,6 @@ GITEE_TAG_TIMEOUT_SECONDS="$GITEE_TAG_TIMEOUT_SECONDS" \
|
||||
deadline_remaining >/dev/null || err "overall Gitee sync deadline exhausted during tag synchronization"
|
||||
target_commit="$(git rev-parse --verify "${VERSION}^{commit}")"
|
||||
|
||||
api_get() {
|
||||
local configured_max_time="$1" max_time
|
||||
shift
|
||||
max_time="$(bounded_max_time "$configured_max_time")" || return 1
|
||||
curl -fsSL --connect-timeout "$GITEE_CURL_CONNECT_TIMEOUT" \
|
||||
--max-time "$max_time" "$@"
|
||||
}
|
||||
|
||||
release_id_from_json() {
|
||||
python3 -c 'import json, sys
|
||||
data = json.load(sys.stdin)
|
||||
@@ -136,12 +141,41 @@ elif isinstance(value, str) and value.isdigit() and int(value) > 0:
|
||||
}
|
||||
|
||||
# ── Resolve or create the Gitee release for this tag ──────────────────────────
|
||||
rel_json="$(api_get "$GITEE_RELEASE_LOOKUP_MAX_TIME" \
|
||||
# A transient lookup failure must not be mistaken for a missing release: doing
|
||||
# so can race a duplicate create and hides the actual Gitee availability issue.
|
||||
release_lookup_body="$(mktemp)"
|
||||
trap 'rm -f "$release_lookup_body"' EXIT HUP INT TERM
|
||||
lookup_max_time="$(bounded_max_time "$GITEE_RELEASE_LOOKUP_MAX_TIME")" \
|
||||
|| err "overall Gitee sync deadline exhausted before release lookup"
|
||||
if ! release_status="$(curl -sS --connect-timeout "$GITEE_CURL_CONNECT_TIMEOUT" \
|
||||
--max-time "$lookup_max_time" \
|
||||
--retry "$GITEE_RELEASE_LOOKUP_RETRIES" \
|
||||
--retry-all-errors \
|
||||
--retry-delay "$GITEE_RELEASE_LOOKUP_RETRY_DELAY" \
|
||||
--retry-max-time "$lookup_max_time" \
|
||||
-o "$release_lookup_body" \
|
||||
-w '%{http_code}' \
|
||||
-H "Authorization: token ${GITEE_TOKEN}" \
|
||||
"${base}/releases/tags/${VERSION}" 2>/dev/null || true)"
|
||||
release_id="$(printf '%s' "$rel_json" | release_id_from_json || true)"
|
||||
"${base}/releases/tags/${VERSION}")"; then
|
||||
deadline_remaining >/dev/null \
|
||||
|| err "overall Gitee sync deadline exhausted during release lookup"
|
||||
err "could not query Gitee release ${VERSION} after bounded retries"
|
||||
fi
|
||||
rel_json="$(<"$release_lookup_body")"
|
||||
case "$release_status" in
|
||||
200)
|
||||
release_id="$(printf '%s' "$rel_json" | release_id_from_json || true)"
|
||||
[ -n "$release_id" ] || err "Gitee release lookup returned HTTP 200 without a valid release id"
|
||||
;;
|
||||
404)
|
||||
release_id=""
|
||||
;;
|
||||
*)
|
||||
err "Gitee release lookup returned HTTP ${release_status} for ${VERSION}"
|
||||
;;
|
||||
esac
|
||||
|
||||
if [ -z "$release_id" ]; then
|
||||
if [ "$release_status" = "404" ]; then
|
||||
deadline_remaining >/dev/null || err "overall Gitee sync deadline exhausted during release lookup"
|
||||
echo " No Gitee release for ${VERSION} yet — creating it."
|
||||
create_max_time="$(bounded_max_time "$GITEE_RELEASE_CREATE_MAX_TIME")" \
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
# Bundled Skills Agent Guide
|
||||
|
||||
This file applies to `skills/`. Read the root `AGENTS.md` and
|
||||
[`docs/coding-agent-guide.md`](../docs/coding-agent-guide.md) first. The full
|
||||
authoring contract lives in
|
||||
[`docs/skill-authoring-guide.md`](../docs/skill-authoring-guide.md); this file
|
||||
is the scoped summary.
|
||||
|
||||
## Hard rules
|
||||
|
||||
- `skills/mono` is the stable single-skill layout; `skills/multi/dingtalk-*`
|
||||
are per-product experimental skills; `dws-shared` is the prerequisite.
|
||||
- Keep `SKILL.md` concise: frontmatter (`name`, `description` with triggers +
|
||||
`Distinct from` + 命令前缀, `cli_version`), routing prose, and pointers.
|
||||
Long references go to `references/`, recipes to `scripts/`.
|
||||
- Write safety rules as concise prose; no injected-preamble mechanism exists
|
||||
in this repository.
|
||||
- Every referenced `dws` command must exist in the current binary; run
|
||||
`make skill-command-integrity` before handoff.
|
||||
- Dual-write: CLI behavior changes update skill prose in the same change;
|
||||
skill routing changes check Schema selection hints per
|
||||
[`docs/schema-contributor-guide.md`](../docs/schema-contributor-guide.md).
|
||||
- Do not add new embed roots; `skills/embed.go` embeds `mono` + `multi` only.
|
||||
@@ -22,7 +22,7 @@ cli_version: ">=1.0.15"
|
||||
- 单次批量操作不超过 30 条记录
|
||||
- 所有命令必须**严格遵循**对应产品参考文档里面规定的参数格式(如:如果有参数值,则参数和参数值之间至少用一个空格隔开)
|
||||
- **脚本优先**:[scripts/](./scripts/) 下的 `python scripts/<name>.py` 已封装翻页/轮询/批量逻辑,遇到对应场景(如 AI 表格批量导入导出、AI 应用创建轮询、文档创建后写内容、钉盘目录树等)**优先调用脚本**而非手写多步命令。脚本均支持 `--dry-run` 预览、`--format json` 输出,失败时回退到手动步骤
|
||||
- **实时个人消息事件例外**:用户要监听消息、订阅事件、自动回复消息或事件驱动 Agent 时,必须走 `dws event consume` 长连接,不要写脚本轮询消息历史
|
||||
- **实时个人消息事件例外**:用户要监听消息、订阅事件、自动回复消息或事件驱动 Agent 时,必须走 `dws event consume ... --flatten` 长连接,不要写脚本轮询消息历史
|
||||
|
||||
## Shortcut 与原子命令的使用原则
|
||||
|
||||
@@ -259,7 +259,7 @@ Schema 与 Help 冲突是**契约漂移**,不得静默猜测或把两边字段
|
||||
|
||||
`dev.*` 包含 helper-only 执行面,其中远端 helper 未进入 pinned metadata 时标记为 `composite`,不能伪装成 `local`。`event list` / `event schema` 读取内置目录和 payload 定义,属于 `local`;`event consume` / `event status` / `event stop` 同时编排远端个人订阅控制面与本地 bus/consume,属于 `composite`。实现来源不同,不改变统一查询边界:进入全局 `dws schema` 的命令必须先进入 reviewed CommandRegistry,并由同一 `ToolSpec` 投影到 leaf、产品/分组、`--all` 与 Catalog。不得在查询时重新调用 MCP `tools/list`,也不得把 Cobra 临时合成结果作为第二条 Schema 数据路径。
|
||||
|
||||
事件需要区分两种 Schema:`dws event schema <event_key>` 查询事件 payload 字段;`dws schema "event consume"` 查询 CLI 命令参数。前者是真实业务命令,后者只读取最终内嵌 SchemaRegistry;不能相互替代。
|
||||
事件需要区分两种 Schema:`dws event schema <event_key> --flatten` 查询 Agent 要消费的顶层业务字段;`dws schema "event consume"` 查询 CLI 命令参数。前者是真实业务命令,后者只读取最终内嵌 SchemaRegistry;不能相互替代。
|
||||
|
||||
`source` 表示最终命令 identity 的来源,不表示运行时 backing;helper/local/MCP 实现机制读取 `interface_mode`、`availability` 和 provenance,不要假定 `dev.*` 必然是 `source=mcp:<server>`,也不要假定本地命令必然是 `source=cobra`。
|
||||
|
||||
|
||||
@@ -13,8 +13,8 @@
|
||||
|
||||
| Command | Purpose |
|
||||
|---|---|
|
||||
| `dws event schema <event_key>` | 查看事件参数和输出字段 schema |
|
||||
| `dws event consume <event_key> [flags]` | 阻塞消费,事件写到 stdout,用 `-f ndjson` |
|
||||
| `dws event schema <event_key> --flatten` | 查看 Agent 使用的顶层业务字段 schema |
|
||||
| `dws event consume <event_key> --flatten [flags]` | 阻塞消费,事件写到 stdout,用 `-f ndjson` |
|
||||
| `dws event status --event <event_key>` | 查看个人订阅、bus、本地 consume |
|
||||
| `dws event stop <subscribe_id> --dry-run` / `--yes` | 先预览,再确认取消订阅并停止对应本地消费 |
|
||||
| `dws event stop --all --dry-run` / `--yes` | 先预览,再确认清理当前身份下全部个人订阅 |
|
||||
@@ -42,20 +42,20 @@
|
||||
|
||||
| 用户说 | 下一步 |
|
||||
|---|---|
|
||||
| "监听有人 @ 我的消息" | `event consume`,事件码 `user_im_message_receive_at`,参数 `-f ndjson` |
|
||||
| "监听我和 userId test-user-001 的单聊消息" | `event consume`,事件码 `user_im_message_receive_o2o`,参数 `--user test-user-001 -f ndjson` |
|
||||
| "监听我和 openDingtalkId abc 的单聊消息" | `event consume`,事件码 `user_im_message_receive_o2o`,参数 `--open-dingtalk-id abc -f ndjson` |
|
||||
| "监听有人 @ 我的消息" | `event consume`,事件码 `user_im_message_receive_at`,参数 `--flatten -f ndjson` |
|
||||
| "监听我和 userId test-user-001 的单聊消息" | `event consume`,事件码 `user_im_message_receive_o2o`,参数 `--user test-user-001 --flatten -f ndjson` |
|
||||
| "监听我和 openDingtalkId abc 的单聊消息" | `event consume`,事件码 `user_im_message_receive_o2o`,参数 `--open-dingtalk-id abc --flatten -f ndjson` |
|
||||
| "监听 XX 群消息" | 先 `dws chat search --query "XX" --format json`,确认后 consume group |
|
||||
| "监听 userId test-user-001 发给我的消息" | `event consume`,事件码 `user_im_message_receive_user`,参数 `--user test-user-001 -f ndjson` |
|
||||
| "监听 openDingtalkId abc 发给我的消息" | `event consume`,事件码 `user_im_message_receive_user`,参数 `--open-dingtalk-id abc -f ndjson` |
|
||||
| "监听我发给 userId test-user-001 的消息是否已读" | `event consume`,事件码 `user_im_message_read_o2o`,参数 `--user test-user-001 -f ndjson` |
|
||||
| "监听 userId test-user-001 发给我的消息" | `event consume`,事件码 `user_im_message_receive_user`,参数 `--user test-user-001 --flatten -f ndjson` |
|
||||
| "监听 openDingtalkId abc 发给我的消息" | `event consume`,事件码 `user_im_message_receive_user`,参数 `--open-dingtalk-id abc --flatten -f ndjson` |
|
||||
| "监听我发给 userId test-user-001 的消息是否已读" | `event consume`,事件码 `user_im_message_read_o2o`,参数 `--user test-user-001 --flatten -f ndjson` |
|
||||
| "监听 XX 群消息已读" | 先解析群 ID,再 consume `user_im_message_read_group --group <id>` |
|
||||
| "监听我和 userId test-user-001 的消息撤回" | `event consume`,事件码 `user_im_message_recall_o2o`,参数 `--user test-user-001 -f ndjson` |
|
||||
| "监听我和 userId test-user-001 的消息撤回" | `event consume`,事件码 `user_im_message_recall_o2o`,参数 `--user test-user-001 --flatten -f ndjson` |
|
||||
| "监听 XX 群消息撤回" | 先解析群 ID,再 consume `user_im_message_recall_group --group <id>` |
|
||||
| "监听我和 userId test-user-001 的消息贴表情" | `event consume`,事件码 `user_im_message_reaction_o2o`,参数 `--user test-user-001 -f ndjson` |
|
||||
| "监听我和 userId test-user-001 的消息贴表情" | `event consume`,事件码 `user_im_message_reaction_o2o`,参数 `--user test-user-001 --flatten -f ndjson` |
|
||||
| "监听 XX 群消息表情回应" | 先解析群 ID,再 consume `user_im_message_reaction_group --group <id>` |
|
||||
| "监听并自动回复某人的单聊消息" | 先解析对端 userId,再启动 o2o consume;不要写轮询脚本 |
|
||||
| "查看个人消息事件 schema" | `dws event schema <event_key>` |
|
||||
| "查看个人消息事件 schema" | `dws event schema <event_key> --flatten` |
|
||||
| "看个人事件订阅状态" | `dws event status --event <event_key>` |
|
||||
| "停止这个个人事件订阅" | `dws event stop <subscribe_id> --dry-run`,确认后改用 `--yes` |
|
||||
|
||||
@@ -66,8 +66,8 @@
|
||||
## Call flow
|
||||
|
||||
1. 从用户意图选择事件码;人名或群名先解析成必填 ID。
|
||||
2. 需要了解字段时运行 `dws event schema <event_key>`,读取 `schema.properties`;`jq_root_path` 当前固定为 `.`。
|
||||
3. 启动 `dws event consume <event_key> ... -f ndjson`,等待 stderr 出现 `[event] ready event_key=<key> bus_pid=<pid> subscribe_id=<id>` 后开始处理 stdout,不要用 `sleep` 猜测。
|
||||
2. 需要了解字段时运行 `dws event schema <event_key> --flatten`,读取 `schema.properties`;此模式的 `jq_root_path` 为 `.`。
|
||||
3. 启动 `dws event consume <event_key> ... --flatten -f ndjson`,等待 stderr 出现 `[event] ready event_key=<key> bus_pid=<pid> subscribe_id=<id>` 后开始处理 stdout,不要用 `sleep` 猜测。
|
||||
4. stdout 每行是一个扁平事件 JSON;直接按该事件的 `schema.properties` 读取顶层字段。
|
||||
5. 需要确认监听状态时运行 `dws event status --event <event_key>`,查看 `Subscriptions` 和 `Consumers`。
|
||||
6. 任务完成后优雅结束 consume;本次新建的订阅会自动取消。复用已有订阅或需要从外部主动取消时,先运行 `dws event stop <subscribe_id> --dry-run`,向用户确认后再以 `--yes` 执行;自测可在 consume 加 `--max-events` 或 `--duration` 自动退出。
|
||||
@@ -75,31 +75,31 @@
|
||||
## Commands
|
||||
|
||||
```bash
|
||||
dws event schema user_im_message_receive_at
|
||||
dws event schema user_im_message_receive_o2o
|
||||
dws event schema user_im_message_receive_group
|
||||
dws event schema user_im_message_receive_user
|
||||
dws event schema user_im_message_read_o2o
|
||||
dws event schema user_im_message_read_group
|
||||
dws event schema user_im_message_recall_o2o
|
||||
dws event schema user_im_message_recall_group
|
||||
dws event schema user_im_message_reaction_o2o
|
||||
dws event schema user_im_message_reaction_group
|
||||
dws event schema user_im_message_receive_at --flatten
|
||||
dws event schema user_im_message_receive_o2o --flatten
|
||||
dws event schema user_im_message_receive_group --flatten
|
||||
dws event schema user_im_message_receive_user --flatten
|
||||
dws event schema user_im_message_read_o2o --flatten
|
||||
dws event schema user_im_message_read_group --flatten
|
||||
dws event schema user_im_message_recall_o2o --flatten
|
||||
dws event schema user_im_message_recall_group --flatten
|
||||
dws event schema user_im_message_reaction_o2o --flatten
|
||||
dws event schema user_im_message_reaction_group --flatten
|
||||
```
|
||||
|
||||
```bash
|
||||
dws event consume user_im_message_receive_at -f ndjson
|
||||
dws event consume user_im_message_receive_o2o --user test-user-001 -f ndjson
|
||||
dws event consume user_im_message_receive_o2o --open-dingtalk-id abc -f ndjson
|
||||
dws event consume user_im_message_receive_group --group <openConversationId> -f ndjson
|
||||
dws event consume user_im_message_receive_user --user test-user-001 -f ndjson
|
||||
dws event consume user_im_message_receive_user --open-dingtalk-id abc -f ndjson
|
||||
dws event consume user_im_message_read_o2o --user test-user-001 -f ndjson
|
||||
dws event consume user_im_message_read_group --group <openConversationId> -f ndjson
|
||||
dws event consume user_im_message_recall_o2o --user test-user-001 -f ndjson
|
||||
dws event consume user_im_message_recall_group --group <openConversationId> -f ndjson
|
||||
dws event consume user_im_message_reaction_o2o --user test-user-001 -f ndjson
|
||||
dws event consume user_im_message_reaction_group --group <openConversationId> -f ndjson
|
||||
dws event consume user_im_message_receive_at --flatten -f ndjson
|
||||
dws event consume user_im_message_receive_o2o --user test-user-001 --flatten -f ndjson
|
||||
dws event consume user_im_message_receive_o2o --open-dingtalk-id abc --flatten -f ndjson
|
||||
dws event consume user_im_message_receive_group --group <openConversationId> --flatten -f ndjson
|
||||
dws event consume user_im_message_receive_user --user test-user-001 --flatten -f ndjson
|
||||
dws event consume user_im_message_receive_user --open-dingtalk-id abc --flatten -f ndjson
|
||||
dws event consume user_im_message_read_o2o --user test-user-001 --flatten -f ndjson
|
||||
dws event consume user_im_message_read_group --group <openConversationId> --flatten -f ndjson
|
||||
dws event consume user_im_message_recall_o2o --user test-user-001 --flatten -f ndjson
|
||||
dws event consume user_im_message_recall_group --group <openConversationId> --flatten -f ndjson
|
||||
dws event consume user_im_message_reaction_o2o --user test-user-001 --flatten -f ndjson
|
||||
dws event consume user_im_message_reaction_group --group <openConversationId> --flatten -f ndjson
|
||||
```
|
||||
|
||||
上述所有 `*_o2o` 命令和 `user_im_message_receive_user` 都可将 `--user <userId>` 替换为 `--open-dingtalk-id <openDingtalkId>`,但两个参数不能同时使用。
|
||||
@@ -125,10 +125,10 @@ dws event stop --all --yes
|
||||
|
||||
## Output parsing
|
||||
|
||||
- 推荐 `-f ndjson`:一行一个事件 JSON,适合 Agent 管道读取。
|
||||
- 人工取样可用 `-f json --max-events 1`。
|
||||
- `jq_root_path` 当前为 `.`;消息正文、发送人和会话 ID 分别直接读取顶层 `content`、`sender`、`conversation_id`。
|
||||
- 不要生成 `fromjson` 或内部 payload 路径。正常处理直接持续读取 stdout,不要改写为 `--output-dir` watcher。
|
||||
- 推荐 `--flatten -f ndjson`:顶层业务字段,一行一个事件 JSON,适合 Agent 管道读取。
|
||||
- 人工取样可用 `--flatten -f json --max-events 1`。`--format` 只控制序列化,`--flatten` 控制数据结构。
|
||||
- `--flatten` 的 `jq_root_path` 为 `.`;消息正文、发送人和会话 ID 分别直接读取顶层 `content`、`sender`、`conversation_id`。
|
||||
- Agent 已显式使用 `--flatten`,不要再生成 `fromjson` 或内部 payload 路径。不传时默认保持兼容 envelope,业务 payload 在 `.data | fromjson`。正常处理直接持续读取 stdout,不要改写为 `--output-dir` watcher。
|
||||
- 群自动回复使用顶层 `conversation_id`;单聊自动回复使用顶层 `sender_open_dingtalk_id`。
|
||||
- 已读事件直接读取 `reader/reader_open_dingtalk_id/read_time`;撤回事件读取 `recaller/recaller_open_dingtalk_id/recall_time`。
|
||||
- 表情回应事件直接读取 `operator/operator_open_dingtalk_id/reaction_name/reaction_text/operation_type/operation_time`。
|
||||
@@ -136,7 +136,7 @@ dws event stop --all --yes
|
||||
- 正常动作事件输出不含内部 `payload/uid/corpid/clientId/filterSubId/bizid`;原始排查才使用 `-f raw` 或 `--debug-raw-events`。
|
||||
- 自己发的消息不作为事件回来(`isSelfLoop` 过滤);自发验证会看到 0 事件,测试投递使用别人或机器人发消息。
|
||||
- `--jq <表达式>` 可进一步过滤或投影扁平输出。
|
||||
- `--debug-raw-events` 仅用于服务端联调,正常消费不要使用。
|
||||
- `--debug-raw-events` 仅用于服务端联调,正常消费不要使用;它和 `--flatten` 互斥,`-f raw` 也不能与 `--flatten` 同时使用。
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
|
||||
@@ -16,8 +16,6 @@ metadata:
|
||||
|
||||
> **PREREQUISITE:** Read the `dws-shared` skill first for auth, global flags, product routing, URL preflight, error codes, and safety rules. The `dws` binary must be on PATH.
|
||||
|
||||
<!-- SAFETY_PREAMBLE_INJECT -->
|
||||
|
||||
> ⚠️ **命令可用性以当前 dws 二进制为准**。服务发现已下线,本文档随内置 skill 发布;如果 `dws <cmd> --help` 不存在,说明当前版本未暴露该命令。若命令存在但调用失败,请按错误中的 endpoint 或 tool 提示确认静态端点目录和后端工具注册。实际调用前可用 `dws <cmd> --help` 或 `--dry-run` 验证。
|
||||
|
||||
> 命令参考:[agoal.md](references/agoal.md)。
|
||||
|
||||
@@ -16,8 +16,6 @@ metadata:
|
||||
|
||||
> **PREREQUISITE:** Read the `dws-shared` skill first for auth, global flags, product routing, URL preflight, error codes, and safety rules. The `dws` binary must be on PATH.
|
||||
|
||||
<!-- SAFETY_PREAMBLE_INJECT -->
|
||||
|
||||
> ⚠️ **命令可用性以当前 dws 二进制为准**。服务发现已下线,本文档随内置 skill 发布;如果 `dws <cmd> --help` 不存在,说明当前版本未暴露该命令。若命令存在但调用失败,请按错误中的 endpoint 或 tool 提示确认静态端点目录和后端工具注册。实际调用前可用 `dws <cmd> --help` 或 `--dry-run` 验证。
|
||||
|
||||
|
||||
|
||||
@@ -16,8 +16,6 @@ metadata:
|
||||
|
||||
> **PREREQUISITE:** Read the root `dws` skill first for auth, global flags, product routing, URL preflight, error codes, and safety rules. The `dws` binary must be on PATH.
|
||||
|
||||
<!-- SAFETY_PREAMBLE_INJECT -->
|
||||
|
||||
> ⚠️ **命令可用性以当前 dws 二进制为准**。服务发现已下线,本文档随内置 skill 发布;如果 `dws <cmd> --help` 不存在,说明当前版本未暴露该命令。若命令存在但调用失败,请按错误中的 endpoint 或 tool 提示确认静态端点目录和后端工具注册。实际调用前可用 `dws <cmd> --help` 或 `--dry-run` 验证。
|
||||
|
||||
|
||||
|
||||
@@ -16,8 +16,6 @@ metadata:
|
||||
|
||||
> **PREREQUISITE:** Read the `dws-shared` skill first for auth, global flags, product routing, URL preflight, error codes, and safety rules. The `dws` binary must be on PATH.
|
||||
|
||||
<!-- SAFETY_PREAMBLE_INJECT -->
|
||||
|
||||
> ⚠️ **命令可用性以当前 dws 二进制为准**。服务发现已下线,本文档随内置 skill 发布;如果 `dws <cmd> --help` 不存在,说明当前版本未暴露该命令。若命令存在但调用失败,请按错误中的 endpoint 或 tool 提示确认静态端点目录和后端工具注册。实际调用前可用 `dws <cmd> --help` 或 `--dry-run` 验证。
|
||||
|
||||
|
||||
|
||||
@@ -16,8 +16,6 @@ metadata:
|
||||
|
||||
> **PREREQUISITE:** Read the `dws-shared` skill first for auth, global flags, product routing, URL preflight, error codes, and safety rules. The `dws` binary must be on PATH.
|
||||
|
||||
<!-- SAFETY_PREAMBLE_INJECT -->
|
||||
|
||||
> ⚠️ **命令可用性以当前 dws 二进制为准**。服务发现已下线,本文档随内置 skill 发布;如果 `dws <cmd> --help` 不存在,说明当前版本未暴露该命令。若命令存在但调用失败,请按错误中的 endpoint 或 tool 提示确认静态端点目录和后端工具注册。实际调用前可用 `dws <cmd> --help` 或 `--dry-run` 验证。
|
||||
|
||||
|
||||
|
||||
@@ -16,8 +16,6 @@ metadata:
|
||||
|
||||
> **PREREQUISITE:** Read the `dws-shared` skill first for auth, global flags, product routing, URL preflight, error codes, and safety rules. The `dws` binary must be on PATH.
|
||||
|
||||
<!-- SAFETY_PREAMBLE_INJECT -->
|
||||
|
||||
> ⚠️ **命令可用性以当前 dws 二进制为准**。服务发现已下线,本文档随内置 skill 发布;如果 `dws <cmd> --help` 不存在,说明当前版本未暴露该命令。若命令存在但调用失败,请按错误中的 endpoint 或 tool 提示确认静态端点目录和后端工具注册。实际调用前可用 `dws <cmd> --help` 或 `--dry-run` 验证。
|
||||
|
||||
|
||||
|
||||
@@ -16,8 +16,6 @@ metadata:
|
||||
|
||||
> **PREREQUISITE:** Read the `dws-shared` skill first for auth, global flags, product routing, URL preflight, error codes, and safety rules. The `dws` binary must be on PATH.
|
||||
|
||||
<!-- SAFETY_PREAMBLE_INJECT -->
|
||||
|
||||
> ⚠️ **命令可用性以当前 dws 二进制为准**。服务发现已下线,本文档随内置 skill 发布;如果 `dws <cmd> --help` 不存在,说明当前版本未暴露该命令。若命令存在但调用失败,请按错误中的 endpoint 或 tool 提示确认静态端点目录和后端工具注册。实际调用前可用 `dws <cmd> --help` 或 `--dry-run` 验证。
|
||||
|
||||
|
||||
|
||||
@@ -16,8 +16,6 @@ metadata:
|
||||
|
||||
> **PREREQUISITE:** Read the `dws-shared` skill first for auth, global flags, product routing, URL preflight, error codes, and safety rules. The `dws` binary must be on PATH.
|
||||
|
||||
<!-- SAFETY_PREAMBLE_INJECT -->
|
||||
|
||||
> ⚠️ **命令可用性以当前 dws 二进制为准**。服务发现已下线,本文档随内置 skill 发布;如果 `dws <cmd> --help` 不存在,说明当前版本未暴露该命令。若命令存在但调用失败,请按错误中的 endpoint 或 tool 提示确认静态端点目录和后端工具注册。实际调用前可用 `dws <cmd> --help` 或 `--dry-run` 验证。
|
||||
|
||||
|
||||
|
||||
@@ -16,8 +16,6 @@ metadata:
|
||||
|
||||
> **PREREQUISITE:** Read the `dws-shared` skill first for auth, global flags, product routing, URL preflight, error codes, and safety rules. The `dws` binary must be on PATH.
|
||||
|
||||
<!-- SAFETY_PREAMBLE_INJECT -->
|
||||
|
||||
> ⚠️ **命令可用性以当前 dws 二进制为准**。服务发现已下线,本文档随内置 skill 发布;如果 `dws <cmd> --help` 不存在,说明当前版本未暴露该命令。若命令存在但调用失败,请按错误中的 endpoint 或 tool 提示确认静态端点目录和后端工具注册。实际调用前可用 `dws <cmd> --help` 或 `--dry-run` 验证。
|
||||
|
||||
|
||||
|
||||
@@ -16,8 +16,6 @@ metadata:
|
||||
|
||||
> **PREREQUISITE:** Read the `dws-shared` skill first for auth, global flags, product routing, URL preflight, error codes, and safety rules. The `dws` binary must be on PATH.
|
||||
|
||||
<!-- SAFETY_PREAMBLE_INJECT -->
|
||||
|
||||
> ⚠️ **命令可用性以当前 dws 二进制为准**。服务发现已下线,本文档随内置 skill 发布;如果 `dws <cmd> --help` 不存在,说明当前版本未暴露该命令。若命令存在但调用失败,请按错误中的 endpoint 或 tool 提示确认静态端点目录和后端工具注册。实际调用前可用 `dws <cmd> --help` 或 `--dry-run` 验证。
|
||||
|
||||
|
||||
|
||||
@@ -16,8 +16,6 @@ metadata:
|
||||
|
||||
> **PREREQUISITE:** Read the `dws-shared` skill first for auth, global flags, product routing, URL preflight, error codes, and safety rules. The `dws` binary must be on PATH.
|
||||
|
||||
<!-- SAFETY_PREAMBLE_INJECT -->
|
||||
|
||||
> ⚠️ **命令可用性以当前 dws 二进制为准**。服务发现已下线,本文档随内置 skill 发布;如果 `dws <cmd> --help` 不存在,说明当前版本未暴露该命令。若命令存在但调用失败,请按错误中的 endpoint 或 tool 提示确认静态端点目录和后端工具注册。实际调用前可用 `dws <cmd> --help` 或 `--dry-run` 验证。
|
||||
|
||||
|
||||
|
||||
@@ -19,8 +19,8 @@ description: 钉钉个人 IM 事件长连接监听、订阅与消费,覆盖消
|
||||
| Command | Purpose |
|
||||
|---|---|
|
||||
| `dws event list` | 查看当前个人事件目录;不要把它当能力菜单主动展示 |
|
||||
| `dws event schema <event_key>` | 查看事件参数和输出字段 schema,默认 JSON |
|
||||
| `dws event consume <event_key> [flags]` | 阻塞消费;事件写到 stdout,推荐 `-f ndjson` |
|
||||
| `dws event schema <event_key> --flatten` | 查看 Agent 使用的顶层业务字段 schema,默认 JSON |
|
||||
| `dws event consume <event_key> --flatten [flags]` | 阻塞消费;事件写到 stdout,推荐 `-f ndjson` |
|
||||
| `dws event status --event <event_key>` | 查看个人订阅、personal bus 和本地 consume |
|
||||
| `dws event stop <subscribe_id> --dry-run` / `--yes` | 先预览,再确认取消个人订阅并停止对应本地消费 |
|
||||
| `dws event stop --all --dry-run` / `--yes` | 先预览,再确认清理当前身份下本地记录的全部个人订阅 |
|
||||
@@ -57,17 +57,17 @@ description: 钉钉个人 IM 事件长连接监听、订阅与消费,覆盖消
|
||||
- 用户只给群名时,先运行 `dws chat search --query "<group>" --format json` 解析 openConversationId;多候选必须让用户确认。
|
||||
- 用户要求执行“撤回消息”时使用 `dws chat`;只有“监听/订阅消息撤回”才使用 `dws event consume user_im_message_recall_*`。
|
||||
- 用户说“贴标签”且语义是给消息贴表情时,按消息表情回应事件处理,event key 使用 `reaction`。
|
||||
- 正常 Agent 消费使用 `-f ndjson`。抓一条样本可用 `--max-events 1 -f json`。
|
||||
- 正常 Agent 消费统一显式使用 `--flatten -f ndjson`。抓一条样本可用 `--flatten --max-events 1 -f json`。`--format` 只控制 JSON 序列化,`--flatten` 才控制数据结构。
|
||||
- 监听非默认组织时带 `--profile <corpId 或 profile 名>`;漏传会退回默认 profile 而失败。
|
||||
- 自己发的消息不作为事件回来(`isSelfLoop` 过滤):边监听边 `dws chat message send` 回复不成环;测试投递用别人 / 机器人发(自发会看到 0 事件)。
|
||||
- `--debug-raw-events` 只用于联调确认服务端推送是否到达本地连接;正常任务不要使用。
|
||||
- `--debug-raw-events` 只用于联调确认服务端推送是否到达本地连接;正常任务不要使用。它和 `--flatten` 互斥,`-f raw` 也不能与 `--flatten` 同时使用。
|
||||
- 排查:consume 报 bus 启动失败 → 报错已带真实原因,先查 `dws --profile <x> auth status`(非默认组织带对 `--profile`);本地日志见 `~/.dws/events/<edition>/personal_stream/<hash>/bus.log`(`hash` 见 `dws event status` 的 Workdir);有残留先用 `dws event stop --all --dry-run` 预览,确认后加 `--yes` 清理。看着"挂住"无输出多是误加了 `--foreground`(那是跑 bus、不打印事件),去掉即可。
|
||||
|
||||
## Call flow
|
||||
|
||||
1. 从用户意图选择事件码;人名或群名先解析成必填 ID。
|
||||
2. 需要了解字段时运行 `dws event schema <event_key>`,读取 `schema.properties`;`jq_root_path` 当前固定为 `.`。
|
||||
3. 启动 `dws event consume <event_key> ... -f ndjson`,等待 stderr 出现 `[event] ready event_key=<key> bus_pid=<pid> subscribe_id=<id>` 后开始处理 stdout,不要用 `sleep` 猜测。
|
||||
2. 需要了解字段时运行 `dws event schema <event_key> --flatten`,读取 `schema.properties`;此模式的 `jq_root_path` 为 `.`。
|
||||
3. 启动 `dws event consume <event_key> ... --flatten -f ndjson`,等待 stderr 出现 `[event] ready event_key=<key> bus_pid=<pid> subscribe_id=<id>` 后开始处理 stdout,不要用 `sleep` 猜测。
|
||||
4. stdout 每行是一个扁平事件 JSON;直接按该事件的 `schema.properties` 读取顶层字段。
|
||||
5. 需要确认监听状态时运行 `dws event status --event <event_key>`,查看 `Subscriptions` 和 `Consumers`。
|
||||
6. 任务完成后优雅结束 consume;本次新建的订阅会自动取消。复用已有订阅或需要从外部主动取消时,先用 `dws event stop <subscribe_id> --dry-run` 预览,向用户确认后再加 `--yes`;临时测试可用 `--max-events` 或 `--duration` 自动退出。
|
||||
@@ -88,57 +88,67 @@ description: 钉钉个人 IM 事件长连接监听、订阅与消费,覆盖消
|
||||
|
||||
```bash
|
||||
# 当前用户被 @ 的消息
|
||||
dws event consume user_im_message_receive_at -f ndjson
|
||||
dws event consume user_im_message_receive_at --flatten -f ndjson
|
||||
|
||||
# 当前用户与指定用户的单聊消息
|
||||
dws event consume user_im_message_receive_o2o \
|
||||
--user test-user-001 \
|
||||
--flatten \
|
||||
-f ndjson
|
||||
|
||||
# 使用 openDingtalkId 监听外部联系人、机器人或跨组织身份的单聊消息
|
||||
dws event consume user_im_message_receive_o2o \
|
||||
--open-dingtalk-id open-user-1 \
|
||||
--flatten \
|
||||
-f ndjson
|
||||
|
||||
# 指定群聊/会话消息
|
||||
dws event consume user_im_message_receive_group \
|
||||
--group cidxxxxxxxx \
|
||||
--flatten \
|
||||
-f ndjson
|
||||
|
||||
# 指定发送人的消息(单聊和群聊)
|
||||
dws event consume user_im_message_receive_user \
|
||||
--user test-user-001 \
|
||||
--flatten \
|
||||
-f ndjson
|
||||
|
||||
# 使用 openDingtalkId 监听指定发送人的消息
|
||||
dws event consume user_im_message_receive_user \
|
||||
--open-dingtalk-id open-user-1 \
|
||||
--flatten \
|
||||
-f ndjson
|
||||
|
||||
# 指定单聊消息已读
|
||||
dws event consume user_im_message_read_o2o \
|
||||
--user test-user-001 \
|
||||
--flatten \
|
||||
-f ndjson
|
||||
|
||||
# 指定群聊消息撤回
|
||||
dws event consume user_im_message_recall_group \
|
||||
--group cidxxxxxxxx \
|
||||
--flatten \
|
||||
-f ndjson
|
||||
|
||||
# 指定单聊消息收到表情回应
|
||||
dws event consume user_im_message_reaction_o2o \
|
||||
--user test-user-001 \
|
||||
--flatten \
|
||||
-f ndjson
|
||||
|
||||
# 有界自测
|
||||
dws event consume user_im_message_receive_at \
|
||||
--duration 10m \
|
||||
--flatten \
|
||||
-f ndjson
|
||||
|
||||
# 抓一条样本
|
||||
dws event consume user_im_message_receive_o2o \
|
||||
--user test-user-001 \
|
||||
--max-events 1 \
|
||||
--flatten \
|
||||
-f json
|
||||
```
|
||||
|
||||
@@ -146,10 +156,10 @@ dws event consume user_im_message_receive_o2o \
|
||||
|
||||
## 输出处理
|
||||
|
||||
- `dws event schema <event_key>` 是写解析逻辑的依据。
|
||||
- 顶层 `jq_root_path` 说明业务字段起点;当前值是 `.`。
|
||||
- `dws event schema <event_key> --flatten` 是 Agent 写解析逻辑的依据。
|
||||
- `--flatten` 模式的顶层 `jq_root_path` 为 `.`;不传时为兼容存量脚本的 transport envelope,业务 payload 在 `.data | fromjson`。
|
||||
- `schema.properties` 是业务字段列表,例如 `content`、`sender`、`conversation_id`、`message_id`、`event_time`。
|
||||
- 所有公开事件都是扁平业务对象,直接读取顶层字段;不要生成 `fromjson` 或内部 payload 路径。
|
||||
- Agent 命令已显式传 `--flatten`,直接读取顶层字段;不要对该模式再生成 `fromjson` 或内部 payload 路径。
|
||||
- 群自动回复使用事件顶层 `conversation_id`;单聊自动回复使用顶层 `sender_open_dingtalk_id`。
|
||||
- 已读事件读取顶层 `reader`、`reader_open_dingtalk_id`、`read_time`;撤回事件读取 `recaller`、`recaller_open_dingtalk_id`、`recall_time`。
|
||||
- 表情回应事件读取顶层 `operator`、`operator_open_dingtalk_id`、`reaction_name`、`reaction_text`、`operation_type`、`operation_time`。
|
||||
|
||||
@@ -15,19 +15,19 @@ dws auth login
|
||||
查看事件 schema:
|
||||
|
||||
```bash
|
||||
dws event schema user_im_message_receive_at
|
||||
dws event schema user_im_message_receive_o2o
|
||||
dws event schema user_im_message_receive_group
|
||||
dws event schema user_im_message_receive_user
|
||||
dws event schema user_im_message_read_o2o
|
||||
dws event schema user_im_message_read_group
|
||||
dws event schema user_im_message_recall_o2o
|
||||
dws event schema user_im_message_recall_group
|
||||
dws event schema user_im_message_reaction_o2o
|
||||
dws event schema user_im_message_reaction_group
|
||||
dws event schema user_im_message_receive_at --flatten
|
||||
dws event schema user_im_message_receive_o2o --flatten
|
||||
dws event schema user_im_message_receive_group --flatten
|
||||
dws event schema user_im_message_receive_user --flatten
|
||||
dws event schema user_im_message_read_o2o --flatten
|
||||
dws event schema user_im_message_read_group --flatten
|
||||
dws event schema user_im_message_recall_o2o --flatten
|
||||
dws event schema user_im_message_recall_group --flatten
|
||||
dws event schema user_im_message_reaction_o2o --flatten
|
||||
dws event schema user_im_message_reaction_group --flatten
|
||||
```
|
||||
|
||||
schema 默认 JSON。业务字段说明在 `schema.properties`,`jq_root_path` 当前固定为 `.`。
|
||||
schema 默认 JSON。Agent 使用 `--flatten` schema,业务字段在 `schema.properties`,`jq_root_path` 为 `.`。不传 `--flatten` 时查看兼容 transport envelope,其 `jq_root_path` 为 `.data | fromjson`。
|
||||
|
||||
## Event catalog
|
||||
|
||||
@@ -62,61 +62,72 @@ schema 默认 JSON。业务字段说明在 `schema.properties`,`jq_root_path`
|
||||
|
||||
```bash
|
||||
# 被 @ 消息
|
||||
dws event consume user_im_message_receive_at -f ndjson
|
||||
dws event consume user_im_message_receive_at --flatten -f ndjson
|
||||
|
||||
# 指定单聊消息
|
||||
dws event consume user_im_message_receive_o2o \
|
||||
--user test-user-001 \
|
||||
--flatten \
|
||||
-f ndjson
|
||||
|
||||
# 通过 openDingtalkId 指定单聊对端
|
||||
dws event consume user_im_message_receive_o2o \
|
||||
--open-dingtalk-id open-user-1 \
|
||||
--flatten \
|
||||
-f ndjson
|
||||
|
||||
# 指定群消息
|
||||
dws event consume user_im_message_receive_group \
|
||||
--group cidxxxxxxxx \
|
||||
--flatten \
|
||||
-f ndjson
|
||||
|
||||
# 指定发送人的消息(单聊和群聊)
|
||||
dws event consume user_im_message_receive_user \
|
||||
--user test-user-001 \
|
||||
--flatten \
|
||||
-f ndjson
|
||||
|
||||
# 通过 openDingtalkId 指定发送人
|
||||
dws event consume user_im_message_receive_user \
|
||||
--open-dingtalk-id open-user-1 \
|
||||
--flatten \
|
||||
-f ndjson
|
||||
|
||||
# 指定单聊已读事件
|
||||
dws event consume user_im_message_read_o2o \
|
||||
--user test-user-001 \
|
||||
--flatten \
|
||||
-f ndjson
|
||||
|
||||
# 指定群聊已读事件
|
||||
dws event consume user_im_message_read_group \
|
||||
--group cidxxxxxxxx \
|
||||
--flatten \
|
||||
-f ndjson
|
||||
|
||||
# 指定单聊撤回事件
|
||||
dws event consume user_im_message_recall_o2o \
|
||||
--user test-user-001 \
|
||||
--flatten \
|
||||
-f ndjson
|
||||
|
||||
# 指定群聊撤回事件
|
||||
dws event consume user_im_message_recall_group \
|
||||
--group cidxxxxxxxx \
|
||||
--flatten \
|
||||
-f ndjson
|
||||
|
||||
# 指定单聊表情回应事件
|
||||
dws event consume user_im_message_reaction_o2o \
|
||||
--user test-user-001 \
|
||||
--flatten \
|
||||
-f ndjson
|
||||
|
||||
# 指定群聊表情回应事件
|
||||
dws event consume user_im_message_reaction_group \
|
||||
--group cidxxxxxxxx \
|
||||
--flatten \
|
||||
-f ndjson
|
||||
```
|
||||
|
||||
@@ -124,16 +135,16 @@ dws event consume user_im_message_reaction_group \
|
||||
|
||||
| 事件码 | 自测参数 | 触发方式 |
|
||||
|---|---|---|
|
||||
| `user_im_message_receive_at` | `--duration 10m -f ndjson` | 让任意可触达用户在群里 @ 当前登录用户 |
|
||||
| `user_im_message_receive_o2o` | `--user <userId>` 或 `--open-dingtalk-id <id>`,加 `--duration 10m -f ndjson` | 让对端用户给当前登录用户发送单聊消息 |
|
||||
| `user_im_message_receive_group` | `--group <openConversationId> --duration 10m -f ndjson` | 让任意用户在该群发送消息 |
|
||||
| `user_im_message_receive_user` | `--user <userId>` 或 `--open-dingtalk-id <id>`,加 `--duration 10m -f ndjson` | 让指定用户分别在单聊或共同群聊中发送消息 |
|
||||
| `user_im_message_read_o2o` | `--user <userId>` 或 `--open-dingtalk-id <id>`,加 `--duration 10m -f ndjson` | 当前用户给对端发送单聊消息,再让对端打开并阅读 |
|
||||
| `user_im_message_read_group` | `--group <openConversationId> --duration 10m -f ndjson` | 当前用户在群内发送消息,再让群成员打开并阅读 |
|
||||
| `user_im_message_recall_o2o` | `--user <userId>` 或 `--open-dingtalk-id <id>`,加 `--duration 10m -f ndjson` | 在指定单聊中发送并撤回一条消息 |
|
||||
| `user_im_message_recall_group` | `--group <openConversationId> --duration 10m -f ndjson` | 在指定群聊中发送并撤回一条消息 |
|
||||
| `user_im_message_reaction_o2o` | `--user <userId>` 或 `--open-dingtalk-id <id>`,加 `--duration 10m -f ndjson` | 在指定单聊中给消息添加表情回应 |
|
||||
| `user_im_message_reaction_group` | `--group <openConversationId> --duration 10m -f ndjson` | 在指定群聊中给消息添加表情回应 |
|
||||
| `user_im_message_receive_at` | `--flatten --duration 10m -f ndjson` | 让任意可触达用户在群里 @ 当前登录用户 |
|
||||
| `user_im_message_receive_o2o` | `--user <userId>` 或 `--open-dingtalk-id <id>`,加 `--flatten --duration 10m -f ndjson` | 让对端用户给当前登录用户发送单聊消息 |
|
||||
| `user_im_message_receive_group` | `--group <openConversationId> --flatten --duration 10m -f ndjson` | 让任意用户在该群发送消息 |
|
||||
| `user_im_message_receive_user` | `--user <userId>` 或 `--open-dingtalk-id <id>`,加 `--flatten --duration 10m -f ndjson` | 让指定用户分别在单聊或共同群聊中发送消息 |
|
||||
| `user_im_message_read_o2o` | `--user <userId>` 或 `--open-dingtalk-id <id>`,加 `--flatten --duration 10m -f ndjson` | 当前用户给对端发送单聊消息,再让对端打开并阅读 |
|
||||
| `user_im_message_read_group` | `--group <openConversationId> --flatten --duration 10m -f ndjson` | 当前用户在群内发送消息,再让群成员打开并阅读 |
|
||||
| `user_im_message_recall_o2o` | `--user <userId>` 或 `--open-dingtalk-id <id>`,加 `--flatten --duration 10m -f ndjson` | 在指定单聊中发送并撤回一条消息 |
|
||||
| `user_im_message_recall_group` | `--group <openConversationId> --flatten --duration 10m -f ndjson` | 在指定群聊中发送并撤回一条消息 |
|
||||
| `user_im_message_reaction_o2o` | `--user <userId>` 或 `--open-dingtalk-id <id>`,加 `--flatten --duration 10m -f ndjson` | 在指定单聊中给消息添加表情回应 |
|
||||
| `user_im_message_reaction_group` | `--group <openConversationId> --flatten --duration 10m -f ndjson` | 在指定群聊中给消息添加表情回应 |
|
||||
|
||||
stderr 出现固定就绪行 `[event] ready event_key=<key> bus_pid=<pid> subscribe_id=<id>` 表示本地 consume 已连接到事件 bus;父进程等这行再读 stdout。stdout 每行是一个扁平事件 JSON。
|
||||
|
||||
@@ -141,7 +152,8 @@ stderr 出现固定就绪行 `[event] ready event_key=<key> bus_pid=<pid> subscr
|
||||
|
||||
| 参数 | 用途 |
|
||||
|---|---|
|
||||
| `-f ndjson` | 推荐输出,一行一个事件 JSON |
|
||||
| `--flatten` | 将 `ndjson/json/pretty` 的默认 transport envelope(或原 compact processor)投影为 Agent 可直接读取的顶层业务字段;不能与 `-f raw` 或 `--debug-raw-events` 同时使用 |
|
||||
| `-f ndjson` | 控制序列化为一行一个 JSON;不改变数据结构 |
|
||||
| `-f json` | 人工查看单条或少量样本;必须配合 `--max-events` 或 `--duration` |
|
||||
| `--max-events <n>` | 收到 N 条后退出 |
|
||||
| `--duration <duration>` | 到时退出,例如 `30s`、`10m` |
|
||||
@@ -153,11 +165,11 @@ stderr 出现固定就绪行 `[event] ready event_key=<key> bus_pid=<pid> subscr
|
||||
| `--filter-json <json>` | 使用个人事件 Filter DSL 过滤 |
|
||||
| `--debug-raw-events` | 联调用:绕过本地过滤,输出当前 personal stream 实际收到的可解析事件 |
|
||||
|
||||
正常 Agent 消费不要使用 `--debug-raw-events`。它会输出当前连接收到的所有可解析事件,只用于判断服务端是否推到了本机连接。
|
||||
正常 Agent 消费不要使用 `--debug-raw-events`。它会输出当前连接收到的所有可解析事件,只用于判断服务端是否推到了本机连接,并且不能与 `--flatten` 同时使用。
|
||||
|
||||
## Output parsing
|
||||
|
||||
`-f ndjson` 的 stdout 每行就是一个扁平业务事件对象。消息接收事件常见顶层字段:
|
||||
Agent 使用 `--flatten -f ndjson`,stdout 每行是一个扁平业务事件对象。消息接收事件常见顶层字段:
|
||||
|
||||
| 字段 | 说明 |
|
||||
|---|---|
|
||||
@@ -173,7 +185,7 @@ stderr 出现固定就绪行 `[event] ready event_key=<key> bus_pid=<pid> subscr
|
||||
| `create_time` | 消息创建时间 |
|
||||
| `event_time` | 消息事件时间戳 |
|
||||
|
||||
直接按顶层字段解析,不要使用 `fromjson`,也不要依赖内部 transport payload 路径。图片、文件等媒体消息的 `content` 可能是可读描述;需要实际媒体文件时调用 `dws chat message download-media`。
|
||||
在 `--flatten` 模式下直接按顶层字段解析,不要再使用 `fromjson` 或内部 payload 路径。不传 `--flatten` 时保持兼容 transport envelope,字段为 `type/event_type/data/headers`,业务 payload 需从 `.data | fromjson` 读取。图片、文件等媒体消息的 `content` 可能是可读描述;需要实际媒体文件时调用 `dws chat message download-media`。
|
||||
|
||||
所有动作事件都包含顶层 `type`、`event_id`、`timestamp`、`subscribe_id`、`message_id`、`conversation_id`、`sender`、`sender_open_dingtalk_id` 和 `event_time`。各类动作的专有字段如下:
|
||||
|
||||
@@ -204,6 +216,7 @@ stderr 出现固定就绪行 `[event] ready event_key=<key> bus_pid=<pid> subscr
|
||||
dws event consume user_im_message_receive_group \
|
||||
--group cidxxxxxxxx \
|
||||
--query "报警,故障" \
|
||||
--flatten \
|
||||
-f ndjson
|
||||
```
|
||||
|
||||
|
||||
@@ -16,8 +16,6 @@ metadata:
|
||||
|
||||
> **PREREQUISITE:** Read the `dws-shared` skill first for auth, global flags, product routing, URL preflight, error codes, and safety rules. The `dws` binary must be on PATH.
|
||||
|
||||
<!-- SAFETY_PREAMBLE_INJECT -->
|
||||
|
||||
> ⚠️ **命令可用性以当前 dws 二进制为准**。服务发现已下线,本文档随内置 skill 发布;如果 `dws <cmd> --help` 不存在,说明当前版本未暴露该命令。若命令存在但调用失败,请按错误中的 endpoint 或 tool 提示确认静态端点目录和后端工具注册。实际调用前可用 `dws <cmd> --help` 或 `--dry-run` 验证。
|
||||
|
||||
|
||||
|
||||
@@ -16,8 +16,6 @@ metadata:
|
||||
|
||||
> **PREREQUISITE:** Read the `dws-shared` skill first for auth, global flags, product routing, URL preflight, error codes, and safety rules. The `dws` binary must be on PATH.
|
||||
|
||||
<!-- SAFETY_PREAMBLE_INJECT -->
|
||||
|
||||
> ⚠️ **命令可用性以当前 dws 二进制为准**。服务发现已下线,本文档随内置 skill 发布;如果 `dws <cmd> --help` 不存在,说明当前版本未暴露该命令。若命令存在但调用失败,请按错误中的 endpoint 或 tool 提示确认静态端点目录和后端工具注册。实际调用前可用 `dws <cmd> --help` 或 `--dry-run` 验证。
|
||||
|
||||
|
||||
|
||||
@@ -16,8 +16,6 @@ metadata:
|
||||
|
||||
> **PREREQUISITE:** Read the `dws-shared` skill first for auth, global flags, product routing, URL preflight, error codes, and safety rules. The `dws` binary must be on PATH.
|
||||
|
||||
<!-- SAFETY_PREAMBLE_INJECT -->
|
||||
|
||||
> ⚠️ **命令可用性以当前 dws 二进制为准**。服务发现已下线,本文档随内置 skill 发布;如果 `dws <cmd> --help` 不存在,说明当前版本未暴露该命令。若命令存在但调用失败,请按错误中的 endpoint 或 tool 提示确认静态端点目录和后端工具注册。实际调用前可用 `dws <cmd> --help` 或 `--dry-run` 验证。
|
||||
|
||||
|
||||
|
||||
@@ -16,8 +16,6 @@ metadata:
|
||||
|
||||
> **PREREQUISITE:** Read the `dws-shared` skill first for auth, global flags, product routing, URL preflight, error codes, and safety rules. The `dws` binary must be on PATH.
|
||||
|
||||
<!-- SAFETY_PREAMBLE_INJECT -->
|
||||
|
||||
> ⚠️ **命令可用性以当前 dws 二进制为准**。服务发现已下线,本文档随内置 skill 发布;如果 `dws <cmd> --help` 不存在,说明当前版本未暴露该命令。若命令存在但调用失败,请按错误中的 endpoint 或 tool 提示确认静态端点目录和后端工具注册。实际调用前可用 `dws <cmd> --help` 或 `--dry-run` 验证。
|
||||
|
||||
|
||||
|
||||
@@ -16,8 +16,6 @@ metadata:
|
||||
|
||||
> **PREREQUISITE:** Read the `dws-shared` skill first for auth, global flags, product routing, Schema discovery, error handling, and safety rules. The `dws` binary must be on PATH.
|
||||
|
||||
<!-- SAFETY_PREAMBLE_INJECT -->
|
||||
|
||||
> 命令参考:[pat.md](references/pat.md)。
|
||||
|
||||
## 意图表
|
||||
|
||||
@@ -16,8 +16,6 @@ metadata:
|
||||
|
||||
> **PREREQUISITE:** Read the `dws-shared` skill first for auth, global flags, product routing, URL preflight, error codes, and safety rules. The `dws` binary must be on PATH.
|
||||
|
||||
<!-- SAFETY_PREAMBLE_INJECT -->
|
||||
|
||||
dws 可同时登录多个钉钉账号,同一组织也可保留多个账号。一个 profile = 一个 `corpId + userId` 身份;当前 profile 决定本次命令注入哪个身份。
|
||||
|
||||
## 触发条件(命中任一即用本 skill)
|
||||
|
||||
@@ -16,8 +16,6 @@ metadata:
|
||||
|
||||
> **PREREQUISITE:** Read the `dws-shared` skill first for auth, global flags, product routing, URL preflight, error codes, and safety rules. The `dws` binary must be on PATH.
|
||||
|
||||
<!-- SAFETY_PREAMBLE_INJECT -->
|
||||
|
||||
> ⚠️ **命令可用性以当前 dws 二进制为准**。服务发现已下线,本文档随内置 skill 发布;如果 `dws <cmd> --help` 不存在,说明当前版本未暴露该命令。若命令存在但调用失败,请按错误中的 endpoint 或 tool 提示确认静态端点目录和后端工具注册。实际调用前可用 `dws <cmd> --help` 或 `--dry-run` 验证。
|
||||
|
||||
|
||||
|
||||
@@ -16,8 +16,6 @@ metadata:
|
||||
|
||||
> **PREREQUISITE:** Read the `dws-shared` skill first for auth, global flags, product routing, URL preflight, error codes, and safety rules. The `dws` binary must be on PATH.
|
||||
|
||||
<!-- SAFETY_PREAMBLE_INJECT -->
|
||||
|
||||
> ⚠️ **命令可用性以当前 dws 二进制为准**。服务发现已下线,本文档随内置 skill 发布;如果 `dws <cmd> --help` 不存在,说明当前版本未暴露该命令。若命令存在但调用失败,请按错误中的 endpoint 或 tool 提示确认静态端点目录和后端工具注册。实际调用前可用 `dws <cmd> --help` 或 `--dry-run` 验证。
|
||||
|
||||
|
||||
|
||||
@@ -16,8 +16,6 @@ metadata:
|
||||
|
||||
> **PREREQUISITE:** Read the `dws-shared` skill first for auth, global flags, product routing, URL preflight, error codes, and safety rules. The `dws` binary must be on PATH.
|
||||
|
||||
<!-- SAFETY_PREAMBLE_INJECT -->
|
||||
|
||||
> ⚠️ **命令可用性以当前 dws 二进制为准**。服务发现已下线,本文档随内置 skill 发布;如果 `dws <cmd> --help` 不存在,说明当前版本未暴露该命令。若命令存在但调用失败,请按错误中的 endpoint 或 tool 提示确认静态端点目录和后端工具注册。实际调用前可用 `dws <cmd> --help` 或 `--dry-run` 验证。
|
||||
|
||||
|
||||
|
||||
@@ -16,8 +16,6 @@ metadata:
|
||||
|
||||
> **PREREQUISITE:** Read the `dws-shared` skill first for auth, global flags, product routing, URL preflight, error codes, and safety rules. The `dws` binary must be on PATH.
|
||||
|
||||
<!-- SAFETY_PREAMBLE_INJECT -->
|
||||
|
||||
> ⚠️ **命令可用性以当前 dws 二进制为准**。服务发现已下线,本文档随内置 skill 发布;如果 `dws <cmd> --help` 不存在,说明当前版本未暴露该命令。若命令存在但调用失败,请按错误中的 endpoint 或 tool 提示确认静态端点目录和后端工具注册。实际调用前可用 `dws <cmd> --help` 或 `--dry-run` 验证。
|
||||
|
||||
|
||||
|
||||
@@ -16,8 +16,6 @@ metadata:
|
||||
|
||||
> **PREREQUISITE:** Read the `dws-shared` skill first for auth, global flags, product routing, URL preflight, error codes, and safety rules. The `dws` binary must be on PATH.
|
||||
|
||||
<!-- SAFETY_PREAMBLE_INJECT -->
|
||||
|
||||
> ⚠️ **命令可用性以当前 dws 二进制为准**。服务发现已下线,本文档随内置 skill 发布;如果 `dws <cmd> --help` 不存在,说明当前版本未暴露该命令。若命令存在但调用失败,请按错误中的 endpoint 或 tool 提示确认静态端点目录和后端工具注册。实际调用前可用 `dws <cmd> --help` 或 `--dry-run` 验证。
|
||||
|
||||
|
||||
|
||||
+323
-18
@@ -300,7 +300,12 @@ func TestSyncToGiteeRunsTagReleaseCreationAndAssetReconciliationWithinOneBudget(
|
||||
case r.Method == http.MethodGet && r.URL.Path == "/repos/owner/repo/releases/tags/v1.2.3":
|
||||
releaseMu.Lock()
|
||||
releaseLookups++
|
||||
lookupAttempt := releaseLookups
|
||||
releaseMu.Unlock()
|
||||
if lookupAttempt == 1 {
|
||||
http.Error(w, "temporary Gitee outage", http.StatusServiceUnavailable)
|
||||
return
|
||||
}
|
||||
http.NotFound(w, r)
|
||||
case r.Method == http.MethodPost && r.URL.Path == "/repos/owner/repo/releases":
|
||||
if err := r.ParseMultipartForm(1 << 20); err != nil {
|
||||
@@ -336,6 +341,8 @@ func TestSyncToGiteeRunsTagReleaseCreationAndAssetReconciliationWithinOneBudget(
|
||||
"GITEE_TAG_TIMEOUT_SECONDS=5",
|
||||
"GITEE_GIT_TIMEOUT_SECONDS=3",
|
||||
"GITEE_RELEASE_LOOKUP_MAX_TIME=2",
|
||||
"GITEE_RELEASE_LOOKUP_RETRIES=2",
|
||||
"GITEE_RELEASE_LOOKUP_RETRY_DELAY=0",
|
||||
"GITEE_RELEASE_CREATE_MAX_TIME=2",
|
||||
"GITEE_RECONCILE_TIMEOUT_SECONDS=20",
|
||||
"GITEE_CHILD_DEADLINE_RESERVE_SECONDS=1",
|
||||
@@ -357,8 +364,8 @@ func TestSyncToGiteeRunsTagReleaseCreationAndAssetReconciliationWithinOneBudget(
|
||||
}
|
||||
|
||||
releaseMu.Lock()
|
||||
if releaseLookups != 1 || releaseCreates != 1 {
|
||||
t.Errorf("release lookup/create calls = %d/%d, want 1/1", releaseLookups, releaseCreates)
|
||||
if releaseLookups != 2 || releaseCreates != 1 {
|
||||
t.Errorf("release lookup/create calls = %d/%d, want 2/1", releaseLookups, releaseCreates)
|
||||
}
|
||||
releaseMu.Unlock()
|
||||
fake.mu.Lock()
|
||||
@@ -374,11 +381,15 @@ func TestReconcileGiteeAssetsRecoversACommittedUploadWithLostResponse(t *testing
|
||||
scriptPath := mustAbs(t, filepath.Join("..", "..", "scripts", "release", "reconcile-gitee-assets.sh"))
|
||||
distDir := seedGiteeDist(t)
|
||||
fake := newFakeGiteeRelease(true, false)
|
||||
fake.listErrorResponsesAfterUpload = 1
|
||||
fake.listEmptyResponsesAfterUpload = 1
|
||||
server := httptest.NewServer(fake)
|
||||
defer server.Close()
|
||||
|
||||
cmd := exec.Command("bash", scriptPath)
|
||||
cmd.Env = giteeAssetEnv(distDir, server.URL, "2")
|
||||
cmd.Env = append(giteeAssetEnv(distDir, server.URL, "2"),
|
||||
"GITEE_POST_UPLOAD_VERIFY_ATTEMPTS=2",
|
||||
)
|
||||
output, err := cmd.CombinedOutput()
|
||||
if err != nil {
|
||||
t.Fatalf("reconcile-gitee-assets.sh error = %v\noutput:\n%s", err, output)
|
||||
@@ -409,6 +420,167 @@ func TestReconcileGiteeAssetsRecoversACommittedUploadWithLostResponse(t *testing
|
||||
}
|
||||
}
|
||||
|
||||
func TestReconcileGiteeAssetsDoesNotReplayAnAmbiguousUpload(t *testing.T) {
|
||||
scriptPath := mustAbs(t, filepath.Join("..", "..", "scripts", "release", "reconcile-gitee-assets.sh"))
|
||||
distDir := seedGiteeDist(t)
|
||||
fake := newFakeGiteeRelease(true, false)
|
||||
fake.listEmptyResponsesAfterUpload = 3
|
||||
server := httptest.NewServer(fake)
|
||||
defer server.Close()
|
||||
|
||||
cmd := exec.Command("bash", scriptPath)
|
||||
cmd.Env = append(giteeAssetEnv(distDir, server.URL, "2"),
|
||||
"GITEE_POST_UPLOAD_VERIFY_ATTEMPTS=2",
|
||||
)
|
||||
output, err := cmd.CombinedOutput()
|
||||
if err == nil {
|
||||
t.Fatalf("ambiguous upload unexpectedly succeeded:\n%s", output)
|
||||
}
|
||||
if !strings.Contains(string(output), "refusing to replay POST") {
|
||||
t.Fatalf("ambiguous upload did not fail closed:\n%s", output)
|
||||
}
|
||||
|
||||
fake.mu.Lock()
|
||||
defer fake.mu.Unlock()
|
||||
for _, name := range requiredGiteeAssets {
|
||||
if got := fake.uploadCalls[name]; got != 1 {
|
||||
t.Errorf("upload calls for %s = %d, want exactly 1", name, got)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestReconcileGiteeAssetsRetriesATransientListOutage(t *testing.T) {
|
||||
scriptPath := mustAbs(t, filepath.Join("..", "..", "scripts", "release", "reconcile-gitee-assets.sh"))
|
||||
distDir := seedGiteeDist(t)
|
||||
fake := newFakeGiteeRelease(false, false)
|
||||
fake.listFailuresRemaining = 2
|
||||
server := httptest.NewServer(fake)
|
||||
defer server.Close()
|
||||
|
||||
cmd := exec.Command("bash", scriptPath)
|
||||
cmd.Env = append(giteeAssetEnv(distDir, server.URL, "1"),
|
||||
"GITEE_LIST_RETRIES=3",
|
||||
"GITEE_LIST_RETRY_DELAY=1",
|
||||
"GITEE_LIST_RETRY_WINDOW_SECONDS=4",
|
||||
)
|
||||
started := time.Now()
|
||||
output, err := cmd.CombinedOutput()
|
||||
elapsed := time.Since(started)
|
||||
if err != nil {
|
||||
t.Fatalf("transient-list reconciliation error = %v\noutput:\n%s", err, output)
|
||||
}
|
||||
if !strings.Contains(string(output), "Gitee attachment list attempt 2/3 failed; retrying in 1s") {
|
||||
t.Fatalf("transient list failures were not retried visibly:\n%s", output)
|
||||
}
|
||||
if elapsed < 1500*time.Millisecond {
|
||||
t.Fatalf("transient list retry elapsed = %s, want production-style backoff", elapsed)
|
||||
}
|
||||
if !strings.Contains(string(output), "all 8 verified") {
|
||||
t.Fatalf("transient-list reconciliation did not verify every asset:\n%s", output)
|
||||
}
|
||||
|
||||
fake.mu.Lock()
|
||||
defer fake.mu.Unlock()
|
||||
if fake.listFailuresRemaining != 0 {
|
||||
t.Fatalf("unconsumed list failures = %d, want 0", fake.listFailuresRemaining)
|
||||
}
|
||||
for _, name := range requiredGiteeAssets {
|
||||
if got := fake.uploadCalls[name]; got != 1 {
|
||||
t.Errorf("upload calls for %s = %d, want 1", name, got)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestReconcileGiteeAssetsDoesNotBurstRetriesInTheFinalWindowSecond(t *testing.T) {
|
||||
scriptPath := mustAbs(t, filepath.Join("..", "..", "scripts", "release", "reconcile-gitee-assets.sh"))
|
||||
distDir := seedGiteeDist(t)
|
||||
fake := newFakeGiteeRelease(false, false)
|
||||
fake.listFailuresRemaining = 1000
|
||||
server := httptest.NewServer(fake)
|
||||
defer server.Close()
|
||||
|
||||
cmd := exec.Command("bash", scriptPath)
|
||||
cmd.Env = append(giteeAssetEnv(distDir, server.URL, "1"),
|
||||
"GITEE_LIST_RETRIES=24",
|
||||
"GITEE_LIST_RETRY_DELAY=1",
|
||||
"GITEE_LIST_RETRY_WINDOW_SECONDS=1",
|
||||
)
|
||||
output, err := cmd.CombinedOutput()
|
||||
if err == nil {
|
||||
t.Fatalf("permanent list outage unexpectedly succeeded:\n%s", output)
|
||||
}
|
||||
|
||||
fake.mu.Lock()
|
||||
defer fake.mu.Unlock()
|
||||
if fake.listCalls > len(requiredGiteeAssets)+1 {
|
||||
t.Fatalf("list calls = %d, want at most one per asset plus final verification", fake.listCalls)
|
||||
}
|
||||
}
|
||||
|
||||
func TestReconcileGiteeAssetsDiscardsPartialOutputFromAMalformedList(t *testing.T) {
|
||||
scriptPath := mustAbs(t, filepath.Join("..", "..", "scripts", "release", "reconcile-gitee-assets.sh"))
|
||||
distDir := seedGiteeDist(t)
|
||||
fake := newFakeGiteeRelease(false, false)
|
||||
existingName := requiredGiteeAssets[0]
|
||||
existingData, err := os.ReadFile(filepath.Join(distDir, existingName))
|
||||
if err != nil {
|
||||
t.Fatalf("ReadFile(%s) error = %v", existingName, err)
|
||||
}
|
||||
fake.assets[1] = fakeGiteeAsset{id: 1, name: existingName, data: existingData}
|
||||
fake.nextID = 2
|
||||
fake.malformedListResponsesRemaining = 1
|
||||
server := httptest.NewServer(fake)
|
||||
defer server.Close()
|
||||
|
||||
cmd := exec.Command("bash", scriptPath)
|
||||
cmd.Env = append(giteeAssetEnv(distDir, server.URL, "1"),
|
||||
"GITEE_LIST_RETRIES=2",
|
||||
"GITEE_LIST_RETRY_DELAY=0",
|
||||
"GITEE_LIST_RETRY_WINDOW_SECONDS=2",
|
||||
)
|
||||
output, err := cmd.CombinedOutput()
|
||||
if err != nil {
|
||||
t.Fatalf("malformed-list reconciliation error = %v\noutput:\n%s", err, output)
|
||||
}
|
||||
if !strings.Contains(string(output), existingName+" already correct on Gitee") {
|
||||
t.Fatalf("partial failed-list output manufactured a stale or duplicate asset:\n%s", output)
|
||||
}
|
||||
if !strings.Contains(string(output), "all 8 verified") {
|
||||
t.Fatalf("malformed-list reconciliation did not verify every asset:\n%s", output)
|
||||
}
|
||||
|
||||
fake.mu.Lock()
|
||||
defer fake.mu.Unlock()
|
||||
if got := fake.uploadCalls[existingName]; got != 0 {
|
||||
t.Errorf("upload calls for existing %s = %d, want 0", existingName, got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestReconcileGiteeAssetsDisablesExpectContinueForLargeUploads(t *testing.T) {
|
||||
scriptPath := mustAbs(t, filepath.Join("..", "..", "scripts", "release", "reconcile-gitee-assets.sh"))
|
||||
distDir := seedGiteeDist(t)
|
||||
mustWriteFile(
|
||||
t,
|
||||
filepath.Join(distDir, requiredGiteeAssets[0]),
|
||||
[]byte(strings.Repeat("x", 2<<20)),
|
||||
0o644,
|
||||
)
|
||||
fake := newFakeGiteeRelease(false, false)
|
||||
fake.rejectExpectContinue = true
|
||||
server := httptest.NewServer(fake)
|
||||
defer server.Close()
|
||||
|
||||
cmd := exec.Command("bash", scriptPath)
|
||||
cmd.Env = giteeAssetEnv(distDir, server.URL, "1")
|
||||
output, err := cmd.CombinedOutput()
|
||||
if err != nil {
|
||||
t.Fatalf("large-asset reconciliation error = %v\noutput:\n%s", err, output)
|
||||
}
|
||||
if !strings.Contains(string(output), "all 8 verified") {
|
||||
t.Fatalf("large-asset reconciliation did not verify every asset:\n%s", output)
|
||||
}
|
||||
}
|
||||
|
||||
func TestReconcileGiteeAssetsFailsWhenAnyUploadIsMissing(t *testing.T) {
|
||||
scriptPath := mustAbs(t, filepath.Join("..", "..", "scripts", "release", "reconcile-gitee-assets.sh"))
|
||||
distDir := seedGiteeDist(t)
|
||||
@@ -515,7 +687,53 @@ func TestGiteeReleaseWorkflowUsesImmutableTagsAndBoundedRetryBudget(t *testing.T
|
||||
}
|
||||
perAssetSeconds := shellDefaultInt(t, reconciler, "GITEE_ASSET_TIMEOUT_SECONDS")
|
||||
overallSeconds := shellDefaultInt(t, reconciler, "GITEE_OVERALL_TIMEOUT_SECONDS")
|
||||
finalListSeconds := shellDefaultInt(t, reconciler, "GITEE_LIST_MAX_TIME")
|
||||
listSeconds := shellDefaultInt(t, reconciler, "GITEE_LIST_MAX_TIME")
|
||||
listRetries := shellDefaultInt(t, reconciler, "GITEE_LIST_RETRIES")
|
||||
listRetryDelay := shellDefaultInt(t, reconciler, "GITEE_LIST_RETRY_DELAY")
|
||||
listRetryWindow := shellDefaultInt(t, reconciler, "GITEE_LIST_RETRY_WINDOW_SECONDS")
|
||||
uploadSeconds := shellDefaultInt(t, reconciler, "GITEE_UPLOAD_MAX_TIME")
|
||||
verifySeconds := shellDefaultInt(t, reconciler, "GITEE_VERIFY_MAX_TIME")
|
||||
postUploadVerifyAttempts := shellDefaultInt(t, reconciler, "GITEE_POST_UPLOAD_VERIFY_ATTEMPTS")
|
||||
verifyRetryDelay := shellDefaultInt(t, reconciler, "GITEE_VERIFY_RETRY_DELAY")
|
||||
uploadRetryDelay := shellDefaultInt(t, reconciler, "GITEE_UPLOAD_RETRY_DELAY")
|
||||
const minimumLargeAssetUploadSeconds = 1200
|
||||
if uploadSeconds < minimumLargeAssetUploadSeconds {
|
||||
t.Fatalf(
|
||||
"Gitee upload deadline = %ds, want at least %ds for near-10 MiB release assets",
|
||||
uploadSeconds, minimumLargeAssetUploadSeconds,
|
||||
)
|
||||
}
|
||||
const minimumTransientListOutageSeconds = 300
|
||||
if listRetryWindow < minimumTransientListOutageSeconds {
|
||||
t.Fatalf(
|
||||
"Gitee list recovery budget = %ds, want at least %ds for a transient API outage",
|
||||
listRetryWindow, minimumTransientListOutageSeconds,
|
||||
)
|
||||
}
|
||||
if (listRetries-1)*listRetryDelay < listRetryWindow {
|
||||
t.Fatalf(
|
||||
"%d Gitee list attempts with %ds delay cannot span the configured %ds retry window after fast failures",
|
||||
listRetries, listRetryDelay, listRetryWindow,
|
||||
)
|
||||
}
|
||||
completeListRecoveryBudget := listRetryWindow
|
||||
oneSlowSuccessBudget := 2*completeListRecoveryBudget + uploadSeconds + verifySeconds
|
||||
if oneSlowSuccessBudget > perAssetSeconds {
|
||||
t.Fatalf(
|
||||
"one complete slow upload budget = %ds, exceeds per-asset deadline %ds",
|
||||
oneSlowSuccessBudget, perAssetSeconds,
|
||||
)
|
||||
}
|
||||
fastFailureRetryBudget := uploadSeconds + (postUploadVerifyAttempts+3)*listSeconds +
|
||||
verifySeconds + (postUploadVerifyAttempts-1)*verifyRetryDelay + uploadRetryDelay
|
||||
fastFailureRetryBudget += 2 * (completeListRecoveryBudget - listSeconds)
|
||||
if fastFailureRetryBudget > perAssetSeconds {
|
||||
t.Fatalf(
|
||||
"fast-failure retry budget = %ds, exceeds per-asset deadline %ds",
|
||||
fastFailureRetryBudget, perAssetSeconds,
|
||||
)
|
||||
}
|
||||
finalListSeconds := completeListRecoveryBudget
|
||||
completeAssetBudget := perAssetSeconds*len(requiredGiteeAssets) + finalListSeconds
|
||||
if completeAssetBudget > overallSeconds {
|
||||
t.Fatalf(
|
||||
@@ -530,15 +748,16 @@ func TestGiteeReleaseWorkflowUsesImmutableTagsAndBoundedRetryBudget(t *testing.T
|
||||
)
|
||||
}
|
||||
|
||||
completeSyncBudget := tagSeconds + lookupSeconds + createSeconds + reconcileSeconds
|
||||
childDeadlineReserveSeconds := shellDefaultInt(t, syncScript, "GITEE_CHILD_DEADLINE_RESERVE_SECONDS")
|
||||
completeSyncBudget := tagSeconds + lookupSeconds + createSeconds + reconcileSeconds + childDeadlineReserveSeconds
|
||||
if completeSyncBudget > syncSeconds {
|
||||
t.Fatalf(
|
||||
"complete sync budget = %ds (tag=%d + lookup=%d + create=%d + reconcile=%d), exceeds sync deadline %ds",
|
||||
completeSyncBudget, tagSeconds, lookupSeconds, createSeconds, reconcileSeconds, syncSeconds,
|
||||
"complete sync budget = %ds (tag=%d + lookup=%d + create=%d + reconcile=%d + child reserve=%d), exceeds sync deadline %ds",
|
||||
completeSyncBudget, tagSeconds, lookupSeconds, createSeconds, reconcileSeconds, childDeadlineReserveSeconds, syncSeconds,
|
||||
)
|
||||
}
|
||||
|
||||
const workflowReserveMinutes = 5
|
||||
const syncStepReserveMinutes = 4
|
||||
releasePath := mustAbs(t, filepath.Join("..", "..", ".github", "workflows", "release.yml"))
|
||||
releaseData, err := os.ReadFile(releasePath)
|
||||
if err != nil {
|
||||
@@ -550,21 +769,49 @@ func TestGiteeReleaseWorkflowUsesImmutableTagsAndBoundedRetryBudget(t *testing.T
|
||||
"mirror-gitee-release:",
|
||||
[]string{
|
||||
"name: Check out sealed release source",
|
||||
"name: Check out trusted release tooling",
|
||||
"name: Fetch and verify sealed release tag",
|
||||
"name: Restore finalized distribution files",
|
||||
"name: Mirror release to Gitee (China)",
|
||||
},
|
||||
workflowReserveMinutes,
|
||||
17,
|
||||
)
|
||||
releaseSyncStepSeconds := workflowTimeoutMinutesAfter(
|
||||
t, string(releaseData), "mirror-gitee-release:", "name: Mirror release to Gitee (China)",
|
||||
) * 60
|
||||
if syncSeconds+workflowReserveMinutes*60 > releaseSyncStepSeconds {
|
||||
if syncSeconds+syncStepReserveMinutes*60 > releaseSyncStepSeconds {
|
||||
t.Fatalf(
|
||||
"sync deadline %ds plus reserve exceeds release fallback step %ds",
|
||||
syncSeconds, releaseSyncStepSeconds,
|
||||
)
|
||||
}
|
||||
|
||||
assertWorkflowBudget(
|
||||
t,
|
||||
string(releaseData),
|
||||
"repair-channel:",
|
||||
[]string{
|
||||
"name: Check out trusted release tooling",
|
||||
"name: Validate repair version",
|
||||
"name: Verify immutable release authority",
|
||||
"name: Check out sealed release source",
|
||||
"name: Fetch and verify sealed release tag",
|
||||
"name: Require successful Release workflow delivery",
|
||||
"name: Download and verify immutable GitHub Release assets",
|
||||
"name: Mirror release to Gitee (China)",
|
||||
},
|
||||
5,
|
||||
)
|
||||
repairSyncStepSeconds := workflowTimeoutMinutesAfter(
|
||||
t, string(releaseData), "repair-channel:", "name: Mirror release to Gitee (China)",
|
||||
) * 60
|
||||
if syncSeconds+syncStepReserveMinutes*60 > repairSyncStepSeconds {
|
||||
t.Fatalf(
|
||||
"sync deadline %ds plus reserve exceeds repair step %ds",
|
||||
syncSeconds, repairSyncStepSeconds,
|
||||
)
|
||||
}
|
||||
|
||||
localBuildPath := mustAbs(t, filepath.Join("..", "..", "scripts", "release", "build-and-publish-gitee.sh"))
|
||||
localBuildData, err := os.ReadFile(localBuildPath)
|
||||
if err != nil {
|
||||
@@ -731,6 +978,9 @@ func giteeAssetEnv(distDir, apiURL, retries string) []string {
|
||||
"GITEE_RELEASE_ID=1",
|
||||
"GITEE_CURL_CONNECT_TIMEOUT=2",
|
||||
"GITEE_CURL_MAX_TIME=2",
|
||||
"GITEE_LIST_RETRIES=2",
|
||||
"GITEE_LIST_RETRY_DELAY=0",
|
||||
"GITEE_LIST_RETRY_WINDOW_SECONDS=2",
|
||||
"GITEE_UPLOAD_MAX_TIME=2",
|
||||
"GITEE_UPLOAD_RETRIES="+retries,
|
||||
"GITEE_UPLOAD_RETRY_DELAY=0",
|
||||
@@ -747,14 +997,22 @@ type fakeGiteeAsset struct {
|
||||
}
|
||||
|
||||
type fakeGiteeRelease struct {
|
||||
mu sync.Mutex
|
||||
nextID int
|
||||
assets map[int]fakeGiteeAsset
|
||||
uploadCalls map[string]int
|
||||
dropFirstResponse bool
|
||||
droppedResponse bool
|
||||
failUploads bool
|
||||
uploadDelay time.Duration
|
||||
mu sync.Mutex
|
||||
nextID int
|
||||
assets map[int]fakeGiteeAsset
|
||||
uploadCalls map[string]int
|
||||
dropFirstResponse bool
|
||||
droppedResponse bool
|
||||
failUploads bool
|
||||
uploadDelay time.Duration
|
||||
rejectExpectContinue bool
|
||||
listFailuresRemaining int
|
||||
malformedListResponsesRemaining int
|
||||
listErrorResponsesRemaining int
|
||||
listEmptyResponsesRemaining int
|
||||
listErrorResponsesAfterUpload int
|
||||
listEmptyResponsesAfterUpload int
|
||||
listCalls int
|
||||
}
|
||||
|
||||
func newFakeGiteeRelease(dropFirstResponse, failUploads bool) *fakeGiteeRelease {
|
||||
@@ -799,7 +1057,42 @@ func (f *fakeGiteeRelease) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
func (f *fakeGiteeRelease) list(w http.ResponseWriter, r *http.Request) {
|
||||
f.mu.Lock()
|
||||
defer f.mu.Unlock()
|
||||
f.listCalls++
|
||||
if f.listFailuresRemaining > 0 {
|
||||
f.listFailuresRemaining--
|
||||
http.Error(w, "temporary Gitee list outage", http.StatusServiceUnavailable)
|
||||
return
|
||||
}
|
||||
if f.listErrorResponsesRemaining > 0 {
|
||||
f.listErrorResponsesRemaining--
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
_, _ = io.WriteString(w, `{}`)
|
||||
return
|
||||
}
|
||||
if f.listEmptyResponsesRemaining > 0 {
|
||||
f.listEmptyResponsesRemaining--
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
_ = json.NewEncoder(w).Encode([]any{})
|
||||
return
|
||||
}
|
||||
baseURL := requestBaseURL(r)
|
||||
if f.malformedListResponsesRemaining > 0 {
|
||||
f.malformedListResponsesRemaining--
|
||||
for _, asset := range f.assets {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
_ = json.NewEncoder(w).Encode([]any{
|
||||
map[string]any{
|
||||
"id": asset.id,
|
||||
"name": asset.name,
|
||||
"browser_download_url": fmt.Sprintf("%s/download/%d", baseURL, asset.id),
|
||||
},
|
||||
nil,
|
||||
})
|
||||
return
|
||||
}
|
||||
http.Error(w, "malformed-list fixture requires one asset", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
rows := make([]map[string]any, 0, len(f.assets))
|
||||
for _, asset := range f.assets {
|
||||
rows = append(rows, map[string]any{
|
||||
@@ -813,6 +1106,10 @@ func (f *fakeGiteeRelease) list(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
func (f *fakeGiteeRelease) upload(w http.ResponseWriter, r *http.Request) {
|
||||
if f.rejectExpectContinue && r.Header.Get("Expect") != "" {
|
||||
http.Error(w, "Expect: 100-continue is not supported", http.StatusExpectationFailed)
|
||||
return
|
||||
}
|
||||
if err := r.ParseMultipartForm(32 << 20); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
@@ -847,6 +1144,14 @@ func (f *fakeGiteeRelease) upload(w http.ResponseWriter, r *http.Request) {
|
||||
id := f.nextID
|
||||
f.nextID++
|
||||
f.assets[id] = fakeGiteeAsset{id: id, name: header.Filename, data: data}
|
||||
if f.listErrorResponsesAfterUpload > 0 {
|
||||
f.listErrorResponsesRemaining += f.listErrorResponsesAfterUpload
|
||||
f.listErrorResponsesAfterUpload = 0
|
||||
}
|
||||
if f.listEmptyResponsesAfterUpload > 0 {
|
||||
f.listEmptyResponsesRemaining += f.listEmptyResponsesAfterUpload
|
||||
f.listEmptyResponsesAfterUpload = 0
|
||||
}
|
||||
drop := f.dropFirstResponse && !f.droppedResponse
|
||||
if drop {
|
||||
f.droppedResponse = true
|
||||
|
||||
+15
-15
@@ -1000,27 +1000,27 @@ Agent 安装 dws skill 后,仅依据 skill 提供的参考文档,将自然
|
||||
|
||||
**event_event_consume_at_001**
|
||||
- Prompt: 监听有人 @ 我的消息
|
||||
- Expected: `dws event consume user_im_message_receive_at -f ndjson`
|
||||
- Expected: `dws event consume user_im_message_receive_at --flatten -f ndjson`
|
||||
|
||||
#### `dws event consume user_im_message_receive_o2o`
|
||||
|
||||
**event_event_consume_o2o_001**
|
||||
- Prompt: 监听我和 userId test-user-001 的单聊消息
|
||||
- Expected: `dws event consume user_im_message_receive_o2o --user test-user-001 -f ndjson`
|
||||
- Expected: `dws event consume user_im_message_receive_o2o --user test-user-001 --flatten -f ndjson`
|
||||
- Flags: `--user` = `test-user-001`
|
||||
|
||||
**event_event_consume_o2o_open_id_001**
|
||||
- Prompt: 监听我和 openDingtalkId abc 的单聊消息
|
||||
- Expected: `dws event consume user_im_message_receive_o2o --open-dingtalk-id abc -f ndjson`
|
||||
- Expected: `dws event consume user_im_message_receive_o2o --open-dingtalk-id abc --flatten -f ndjson`
|
||||
- Flags: `--open-dingtalk-id` = `abc`
|
||||
|
||||
**event_event_consume_o2o_003** `[ASK_USER]`
|
||||
- Prompt: 监听我的个人单聊消息
|
||||
- Expected: `dws event consume user_im_message_receive_o2o -f ndjson`
|
||||
- Expected: `dws event consume user_im_message_receive_o2o --flatten -f ndjson`
|
||||
|
||||
**event_event_consume_o2o_auto_reply_001**
|
||||
- Prompt: 监听我和 userId test-user-001 的单聊消息,并对方发什么自动回复什么
|
||||
- Expected: `dws event consume user_im_message_receive_o2o --user test-user-001 -f ndjson`
|
||||
- Expected: `dws event consume user_im_message_receive_o2o --user test-user-001 --flatten -f ndjson`
|
||||
- Flags: `--user` = `test-user-001`
|
||||
- Contract: 等待 stderr 的 `[event] ready`;从每行 NDJSON 顶层读取 `content` 和 `sender_open_dingtalk_id`,持续读取 stdout,不使用轮询或 output-dir watcher
|
||||
|
||||
@@ -1028,7 +1028,7 @@ Agent 安装 dws skill 后,仅依据 skill 提供的参考文档,将自然
|
||||
|
||||
**event_event_consume_group_001**
|
||||
- Prompt: 监听 openConversationId cid123 的群消息
|
||||
- Expected: `dws event consume user_im_message_receive_group --group cid123 -f ndjson`
|
||||
- Expected: `dws event consume user_im_message_receive_group --group cid123 --flatten -f ndjson`
|
||||
- Flags: `--group` = `cid123`
|
||||
- Contract: 群自动回复时直接读取事件顶层 `conversation_id`
|
||||
|
||||
@@ -1036,19 +1036,19 @@ Agent 安装 dws skill 后,仅依据 skill 提供的参考文档,将自然
|
||||
|
||||
**event_event_consume_user_001**
|
||||
- Prompt: 监听 userId test-user-001 发给我的消息,包括单聊和群聊
|
||||
- Expected: `dws event consume user_im_message_receive_user --user test-user-001 -f ndjson`
|
||||
- Expected: `dws event consume user_im_message_receive_user --user test-user-001 --flatten -f ndjson`
|
||||
- Flags: `--user` = `test-user-001`
|
||||
|
||||
**event_event_consume_user_open_id_001**
|
||||
- Prompt: 监听 openDingtalkId abc 发给我的消息,包括单聊和群聊
|
||||
- Expected: `dws event consume user_im_message_receive_user --open-dingtalk-id abc -f ndjson`
|
||||
- Expected: `dws event consume user_im_message_receive_user --open-dingtalk-id abc --flatten -f ndjson`
|
||||
- Flags: `--open-dingtalk-id` = `abc`
|
||||
|
||||
#### `dws event consume user_im_message_read_o2o`
|
||||
|
||||
**event_event_consume_read_o2o_001**
|
||||
- Prompt: 监听我发给 userId test-user-001 的单聊消息是否已读
|
||||
- Expected: `dws event consume user_im_message_read_o2o --user test-user-001 -f ndjson`
|
||||
- Expected: `dws event consume user_im_message_read_o2o --user test-user-001 --flatten -f ndjson`
|
||||
- Flags: `--user` = `test-user-001`
|
||||
- Contract: 从每行 NDJSON 顶层读取 `message_id`、`reader`、`reader_open_dingtalk_id`、`read_time`
|
||||
|
||||
@@ -1056,7 +1056,7 @@ Agent 安装 dws skill 后,仅依据 skill 提供的参考文档,将自然
|
||||
|
||||
**event_event_consume_read_group_001**
|
||||
- Prompt: 监听 openConversationId cid123 群里我发的消息是否已读
|
||||
- Expected: `dws event consume user_im_message_read_group --group cid123 -f ndjson`
|
||||
- Expected: `dws event consume user_im_message_read_group --group cid123 --flatten -f ndjson`
|
||||
- Flags: `--group` = `cid123`
|
||||
- Contract: 从每行 NDJSON 顶层读取 `conversation_id`、`reader`、`reader_open_dingtalk_id`、`read_time`
|
||||
|
||||
@@ -1064,7 +1064,7 @@ Agent 安装 dws skill 后,仅依据 skill 提供的参考文档,将自然
|
||||
|
||||
**event_event_consume_recall_o2o_001**
|
||||
- Prompt: 监听我和 userId test-user-001 的单聊消息撤回事件
|
||||
- Expected: `dws event consume user_im_message_recall_o2o --user test-user-001 -f ndjson`
|
||||
- Expected: `dws event consume user_im_message_recall_o2o --user test-user-001 --flatten -f ndjson`
|
||||
- Flags: `--user` = `test-user-001`
|
||||
- Contract: 从每行 NDJSON 顶层读取 `message_id`、`recaller`、`recaller_open_dingtalk_id`、`recall_time`
|
||||
|
||||
@@ -1072,7 +1072,7 @@ Agent 安装 dws skill 后,仅依据 skill 提供的参考文档,将自然
|
||||
|
||||
**event_event_consume_recall_group_001**
|
||||
- Prompt: 监听 openConversationId cid123 的群消息撤回事件
|
||||
- Expected: `dws event consume user_im_message_recall_group --group cid123 -f ndjson`
|
||||
- Expected: `dws event consume user_im_message_recall_group --group cid123 --flatten -f ndjson`
|
||||
- Flags: `--group` = `cid123`
|
||||
- Contract: 从每行 NDJSON 顶层读取 `conversation_id`、`recaller`、`recaller_open_dingtalk_id`、`recall_time`
|
||||
|
||||
@@ -1080,13 +1080,13 @@ Agent 安装 dws skill 后,仅依据 skill 提供的参考文档,将自然
|
||||
|
||||
**event_event_consume_reaction_o2o_001**
|
||||
- Prompt: 监听我和 userId test-user-001 的单聊消息贴表情事件
|
||||
- Expected: `dws event consume user_im_message_reaction_o2o --user test-user-001 -f ndjson`
|
||||
- Expected: `dws event consume user_im_message_reaction_o2o --user test-user-001 --flatten -f ndjson`
|
||||
- Flags: `--user` = `test-user-001`
|
||||
- Contract: 从每行 NDJSON 顶层读取 `operator`、`operator_open_dingtalk_id`、`reaction_name`、`operation_type`
|
||||
|
||||
**event_event_consume_reaction_o2o_open_id_001**
|
||||
- Prompt: 监听我和 openDingtalkId abc 的单聊消息贴表情事件
|
||||
- Expected: `dws event consume user_im_message_reaction_o2o --open-dingtalk-id abc -f ndjson`
|
||||
- Expected: `dws event consume user_im_message_reaction_o2o --open-dingtalk-id abc --flatten -f ndjson`
|
||||
- Flags: `--open-dingtalk-id` = `abc`
|
||||
- Contract: 从每行 NDJSON 顶层读取 `operator`、`operator_open_dingtalk_id`、`reaction_name`、`operation_type`
|
||||
|
||||
@@ -1094,7 +1094,7 @@ Agent 安装 dws skill 后,仅依据 skill 提供的参考文档,将自然
|
||||
|
||||
**event_event_consume_reaction_group_001**
|
||||
- Prompt: 监听 openConversationId cid123 的群消息表情回应事件
|
||||
- Expected: `dws event consume user_im_message_reaction_group --group cid123 -f ndjson`
|
||||
- Expected: `dws event consume user_im_message_reaction_group --group cid123 --flatten -f ndjson`
|
||||
- Flags: `--group` = `cid123`
|
||||
- Contract: 从每行 NDJSON 顶层读取 `conversation_id`、`operator`、`reaction_name`、`reaction_text`、`operation_type`
|
||||
|
||||
|
||||
@@ -95,6 +95,7 @@ func TestEventSkillUsesFlatOutputContract(t *testing.T) {
|
||||
text := string(content)
|
||||
for _, required := range []string{
|
||||
"[event] ready",
|
||||
"--flatten",
|
||||
"conversation_id",
|
||||
"sender_open_dingtalk_id",
|
||||
"reader_open_dingtalk_id",
|
||||
@@ -109,7 +110,6 @@ func TestEventSkillUsesFlatOutputContract(t *testing.T) {
|
||||
}
|
||||
}
|
||||
for _, retired := range []string{
|
||||
".data | fromjson",
|
||||
"payload.body.",
|
||||
"尚无稳定业务样本",
|
||||
"暂无稳定 payload schema",
|
||||
|
||||
Reference in New Issue
Block a user