mirror of
https://github.com/larksuite/cli.git
synced 2026-07-06 16:18:05 +08:00
Compare commits
7 Commits
feat-svgli
...
feat/opt-i
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d48f84c096 | ||
|
|
cbd6e56ac0 | ||
|
|
572eb8da41 | ||
|
|
82a099feaf | ||
|
|
51f2a70e6d | ||
|
|
237a77feb3 | ||
|
|
040ef17eae |
49
.github/workflows/ci.yml
vendored
49
.github/workflows/ci.yml
vendored
@@ -5,7 +5,6 @@ on:
|
||||
branches: [main]
|
||||
pull_request:
|
||||
branches: [main]
|
||||
types: [opened, synchronize, reopened, edited]
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
@@ -71,7 +70,6 @@ jobs:
|
||||
- uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5
|
||||
with:
|
||||
fetch-depth: 0
|
||||
persist-credentials: false
|
||||
- uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6
|
||||
with:
|
||||
go-version-file: go.mod
|
||||
@@ -89,23 +87,6 @@ jobs:
|
||||
- name: Run errs/ lint guards (lintcheck)
|
||||
run: go run -C lint . --changed-from "$QUALITY_GATE_CHANGED_FROM" ..
|
||||
|
||||
script-test:
|
||||
needs: fast-gate
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5
|
||||
with:
|
||||
fetch-depth: 0
|
||||
persist-credentials: false
|
||||
- uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6
|
||||
with:
|
||||
go-version-file: go.mod
|
||||
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
|
||||
with:
|
||||
node-version: '22'
|
||||
- name: Run script tests
|
||||
run: make script-test
|
||||
|
||||
deterministic-gate:
|
||||
needs: fast-gate
|
||||
runs-on: ubuntu-latest
|
||||
@@ -128,28 +109,8 @@ jobs:
|
||||
env:
|
||||
QUALITY_GATE_CHANGED_FROM: ${{ github.event.pull_request.base.sha || github.event.before || 'origin/main' }}
|
||||
run: echo "QUALITY_GATE_CHANGED_FROM=$(bash scripts/resolve-changed-from.sh)" >> "$GITHUB_ENV"
|
||||
- name: Write public content metadata
|
||||
if: ${{ github.event_name == 'pull_request' }}
|
||||
env:
|
||||
PR_TITLE: ${{ github.event.pull_request.title }}
|
||||
PR_BODY: ${{ github.event.pull_request.body }}
|
||||
PR_BRANCH: ${{ github.head_ref }}
|
||||
run: |
|
||||
mkdir -p .tmp/quality-gate
|
||||
python3 - <<'PY'
|
||||
import json
|
||||
import os
|
||||
|
||||
with open(".tmp/quality-gate/public-content-metadata.json", "w", encoding="utf-8") as f:
|
||||
json.dump({
|
||||
"title": os.environ.get("PR_TITLE", ""),
|
||||
"body": os.environ.get("PR_BODY", ""),
|
||||
"branch": os.environ.get("PR_BRANCH", ""),
|
||||
}, f)
|
||||
f.write("\n")
|
||||
PY
|
||||
- name: Run CLI deterministic gate
|
||||
run: PUBLIC_CONTENT_METADATA=.tmp/quality-gate/public-content-metadata.json make quality-gate
|
||||
run: make quality-gate
|
||||
- name: Upload quality gate facts
|
||||
if: ${{ always() && github.event_name == 'pull_request' }}
|
||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
|
||||
@@ -259,7 +220,7 @@ jobs:
|
||||
|
||||
# ── Layer 3: E2E Gate ──────────────────────────────────────────────
|
||||
e2e-dry-run:
|
||||
needs: [unit-test, lint, script-test, deterministic-gate]
|
||||
needs: [unit-test, lint, deterministic-gate]
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5
|
||||
@@ -280,7 +241,7 @@ jobs:
|
||||
run: go test -v -count=1 -timeout=5m ./tests/cli_e2e/... -run 'DryRun|Regression'
|
||||
|
||||
e2e-live:
|
||||
needs: [unit-test, lint, script-test, deterministic-gate]
|
||||
needs: [unit-test, lint, deterministic-gate]
|
||||
if: ${{ github.event_name != 'pull_request' || !github.event.pull_request.head.repo.fork }}
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
@@ -372,7 +333,7 @@ jobs:
|
||||
# ── Results Gate (single required check for branch protection) ─────
|
||||
results:
|
||||
if: ${{ always() }}
|
||||
needs: [fast-gate, unit-test, lint, script-test, deterministic-gate, coverage, deadcode, e2e-dry-run, e2e-live, security, license-header]
|
||||
needs: [fast-gate, unit-test, lint, deterministic-gate, coverage, deadcode, e2e-dry-run, e2e-live, security, license-header]
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Evaluate results
|
||||
@@ -384,7 +345,6 @@ jobs:
|
||||
echo "| L1 | fast-gate | ${{ needs.fast-gate.result }} |" >> $GITHUB_STEP_SUMMARY
|
||||
echo "| L2 | unit-test | ${{ needs.unit-test.result }} |" >> $GITHUB_STEP_SUMMARY
|
||||
echo "| L2 | lint | ${{ needs.lint.result }} |" >> $GITHUB_STEP_SUMMARY
|
||||
echo "| L2 | script-test | ${{ needs.script-test.result }} |" >> $GITHUB_STEP_SUMMARY
|
||||
echo "| L2 | deterministic-gate | ${{ needs.deterministic-gate.result }} |" >> $GITHUB_STEP_SUMMARY
|
||||
echo "| L2 | coverage | ${{ needs.coverage.result }} |" >> $GITHUB_STEP_SUMMARY
|
||||
echo "| L2 | deadcode | ${{ needs.deadcode.result }} |" >> $GITHUB_STEP_SUMMARY
|
||||
@@ -401,7 +361,6 @@ jobs:
|
||||
"${{ needs.fast-gate.result }}" \
|
||||
"${{ needs.unit-test.result }}" \
|
||||
"${{ needs.lint.result }}" \
|
||||
"${{ needs.script-test.result }}" \
|
||||
"${{ needs.deterministic-gate.result }}" \
|
||||
"${{ needs.coverage.result }}" \
|
||||
"${{ needs.deadcode.result }}" \
|
||||
|
||||
28
.github/workflows/comment-audit.yml
vendored
28
.github/workflows/comment-audit.yml
vendored
@@ -1,28 +0,0 @@
|
||||
name: Comment Audit
|
||||
|
||||
on:
|
||||
issue_comment:
|
||||
types: [created, edited]
|
||||
pull_request_review:
|
||||
types: [submitted, edited]
|
||||
pull_request_review_comment:
|
||||
types: [created, edited]
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
public-content-comment-audit:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5
|
||||
with:
|
||||
persist-credentials: false
|
||||
- uses: actions/setup-go@4a3601121dd01d1626a1e23e37211e3254c1c06c # v6
|
||||
with:
|
||||
go-version-file: go.mod
|
||||
- name: Post-publication comment audit
|
||||
run: |
|
||||
mkdir -p .tmp/comment-audit
|
||||
cp "$GITHUB_EVENT_PATH" .tmp/comment-audit/event.json
|
||||
go run ./internal/qualitygate/cmd/comment-audit --event .tmp/comment-audit/event.json --kind "$GITHUB_EVENT_NAME"
|
||||
107
.github/workflows/semantic-review.yml
vendored
107
.github/workflows/semantic-review.yml
vendored
@@ -47,13 +47,10 @@ jobs:
|
||||
throw new Error(`ambiguous workflow_run pull request bindings: ${runPRs.length}`);
|
||||
}
|
||||
let prNumber = Number(runPRs[0]?.number || 0);
|
||||
const eventBaseSha = runPRs[0]?.base?.sha || "";
|
||||
let eventBaseSha = runPRs[0]?.base?.sha || "";
|
||||
const eventHeadSha = runPRs[0]?.head?.sha || "";
|
||||
const targetHeadSha = run.head_sha;
|
||||
const targetHeadSha = eventHeadSha || run.head_sha;
|
||||
if (!/^[a-f0-9]{40}$/i.test(targetHeadSha)) throw new Error("invalid PR head sha");
|
||||
if (eventHeadSha && eventHeadSha.toLowerCase() !== targetHeadSha.toLowerCase()) {
|
||||
core.notice("PR quality summary using workflow_run head_sha because workflow_run pull request head differs from the CI run head");
|
||||
}
|
||||
|
||||
const factsArtifactPattern = /^quality-gate-facts-([a-f0-9]{40})-([a-f0-9]{40})$/i;
|
||||
const { data: artifactData } = await github.rest.actions.listWorkflowRunArtifacts({
|
||||
@@ -74,11 +71,11 @@ jobs:
|
||||
if (artifactHeadSha.toLowerCase() !== targetHeadSha.toLowerCase()) {
|
||||
artifactError = "facts artifact head sha does not match verified PR head sha";
|
||||
factsArtifactName = "";
|
||||
} else if (eventBaseSha && parsedBaseSha.toLowerCase() !== eventBaseSha.toLowerCase()) {
|
||||
artifactError = "facts artifact base sha does not match workflow_run pull request base sha";
|
||||
factsArtifactName = "";
|
||||
} else {
|
||||
artifactBaseSha = parsedBaseSha;
|
||||
if (eventBaseSha && parsedBaseSha.toLowerCase() !== eventBaseSha.toLowerCase()) {
|
||||
core.notice("PR quality summary using facts artifact base because workflow_run pull request base differs from the CI facts artifact base");
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!prNumber) {
|
||||
@@ -88,44 +85,31 @@ jobs:
|
||||
commit_sha: targetHeadSha,
|
||||
});
|
||||
const candidatePRs = associatedPRs.filter((candidate) =>
|
||||
candidate.state === "open" &&
|
||||
candidate.base?.repo?.id === context.payload.repository.id &&
|
||||
candidate.head?.sha === targetHeadSha
|
||||
);
|
||||
const openCandidatePRs = candidatePRs.filter((candidate) => candidate.state === "open");
|
||||
if (openCandidatePRs.length > 1) {
|
||||
throw new Error(`ambiguous open PRs for workflow_run head ${targetHeadSha}: ${openCandidatePRs.length}`);
|
||||
if (candidatePRs.length > 1) {
|
||||
throw new Error(`ambiguous open PRs for workflow_run head ${targetHeadSha}: ${candidatePRs.length}`);
|
||||
}
|
||||
if (openCandidatePRs.length === 1) {
|
||||
prNumber = openCandidatePRs[0].number;
|
||||
} else if (candidatePRs.length > 0) {
|
||||
core.notice("PR quality summary skipped: workflow_run target PR is no longer open");
|
||||
core.setOutput("stale", "true");
|
||||
return;
|
||||
if (candidatePRs.length === 1) {
|
||||
prNumber = candidatePRs[0].number;
|
||||
}
|
||||
}
|
||||
if (!prNumber) {
|
||||
const candidatePRs = await github.paginate(github.rest.pulls.list, {
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
state: "all",
|
||||
state: "open",
|
||||
per_page: 100,
|
||||
}).then((prs) => prs.filter((candidate) =>
|
||||
candidate.base?.repo?.id === context.payload.repository.id &&
|
||||
candidate.head?.sha === targetHeadSha
|
||||
));
|
||||
const openCandidatePRs = candidatePRs.filter((candidate) => candidate.state === "open");
|
||||
if (openCandidatePRs.length > 1) {
|
||||
throw new Error(`ambiguous open PRs from pull list fallback for workflow_run head ${targetHeadSha}: ${openCandidatePRs.length}`);
|
||||
}
|
||||
if (openCandidatePRs.length === 1) {
|
||||
prNumber = openCandidatePRs[0].number;
|
||||
} else if (candidatePRs.length > 0) {
|
||||
core.notice("PR quality summary skipped: workflow_run target PR is no longer open");
|
||||
core.setOutput("stale", "true");
|
||||
return;
|
||||
} else {
|
||||
if (candidatePRs.length !== 1) {
|
||||
throw new Error(`expected one open PR from pull list fallback for workflow_run head ${targetHeadSha}, got ${candidatePRs.length}`);
|
||||
}
|
||||
prNumber = candidatePRs[0].number;
|
||||
}
|
||||
if (!Number.isInteger(prNumber) || prNumber <= 0) throw new Error("missing pull request binding");
|
||||
const { data: pr } = await github.rest.pulls.get({
|
||||
@@ -134,17 +118,12 @@ jobs:
|
||||
pull_number: prNumber,
|
||||
});
|
||||
if (pr.base.repo.id !== context.payload.repository.id) throw new Error("PR base repo mismatch");
|
||||
if (pr.state !== "open") {
|
||||
core.notice("PR quality summary skipped: workflow_run target PR is no longer open");
|
||||
core.setOutput("stale", "true");
|
||||
return;
|
||||
}
|
||||
if (pr.head.sha !== targetHeadSha) {
|
||||
core.notice("PR quality summary skipped: workflow_run is stale for this PR head");
|
||||
core.setOutput("stale", "true");
|
||||
return;
|
||||
}
|
||||
const baseSha = artifactBaseSha || eventBaseSha || pr.base.sha;
|
||||
const baseSha = eventBaseSha || artifactBaseSha || pr.base.sha;
|
||||
if (!/^[a-f0-9]{40}$/i.test(baseSha)) throw new Error("invalid PR base sha");
|
||||
if ((eventBaseSha || artifactBaseSha) && pr.base.sha !== baseSha) {
|
||||
core.notice("PR quality summary skipped: workflow_run is stale for this PR base");
|
||||
@@ -276,13 +255,10 @@ jobs:
|
||||
throw new Error(`ambiguous workflow_run pull request bindings: ${runPRs.length}`);
|
||||
}
|
||||
let prNumber = Number(runPRs[0]?.number || 0);
|
||||
const eventBaseSha = runPRs[0]?.base?.sha || "";
|
||||
let eventBaseSha = runPRs[0]?.base?.sha || "";
|
||||
const eventHeadSha = runPRs[0]?.head?.sha || "";
|
||||
const targetHeadSha = run.head_sha;
|
||||
const targetHeadSha = eventHeadSha || run.head_sha;
|
||||
if (!/^[a-f0-9]{40}$/i.test(targetHeadSha)) throw new Error("invalid PR head sha");
|
||||
if (eventHeadSha && eventHeadSha.toLowerCase() !== targetHeadSha.toLowerCase()) {
|
||||
core.notice("semantic review using workflow_run head_sha because workflow_run pull request head differs from the CI run head");
|
||||
}
|
||||
|
||||
const factsArtifactPattern = /^quality-gate-facts-([a-f0-9]{40})-([a-f0-9]{40})$/i;
|
||||
const { data: artifactData } = await github.rest.actions.listWorkflowRunArtifacts({
|
||||
@@ -303,11 +279,11 @@ jobs:
|
||||
if (artifactHeadSha.toLowerCase() !== targetHeadSha.toLowerCase()) {
|
||||
artifactError = "facts artifact head sha does not match verified PR head sha";
|
||||
factsArtifactName = "";
|
||||
} else if (eventBaseSha && parsedBaseSha.toLowerCase() !== eventBaseSha.toLowerCase()) {
|
||||
artifactError = "facts artifact base sha does not match workflow_run pull request base sha";
|
||||
factsArtifactName = "";
|
||||
} else {
|
||||
artifactBaseSha = parsedBaseSha;
|
||||
if (eventBaseSha && parsedBaseSha.toLowerCase() !== eventBaseSha.toLowerCase()) {
|
||||
core.notice("semantic review using facts artifact base because workflow_run pull request base differs from the CI facts artifact base");
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!prNumber) {
|
||||
@@ -317,44 +293,31 @@ jobs:
|
||||
commit_sha: targetHeadSha,
|
||||
});
|
||||
const candidatePRs = associatedPRs.filter((candidate) =>
|
||||
candidate.state === "open" &&
|
||||
candidate.base?.repo?.id === context.payload.repository.id &&
|
||||
candidate.head?.sha === targetHeadSha
|
||||
);
|
||||
const openCandidatePRs = candidatePRs.filter((candidate) => candidate.state === "open");
|
||||
if (openCandidatePRs.length > 1) {
|
||||
throw new Error(`ambiguous open PRs for workflow_run head ${targetHeadSha}: ${openCandidatePRs.length}`);
|
||||
if (candidatePRs.length > 1) {
|
||||
throw new Error(`ambiguous open PRs for workflow_run head ${targetHeadSha}: ${candidatePRs.length}`);
|
||||
}
|
||||
if (openCandidatePRs.length === 1) {
|
||||
prNumber = openCandidatePRs[0].number;
|
||||
} else if (candidatePRs.length > 0) {
|
||||
core.notice("semantic review skipped: workflow_run target PR is no longer open");
|
||||
core.setOutput("stale", "true");
|
||||
return;
|
||||
if (candidatePRs.length === 1) {
|
||||
prNumber = candidatePRs[0].number;
|
||||
}
|
||||
}
|
||||
if (!prNumber) {
|
||||
const candidatePRs = await github.paginate(github.rest.pulls.list, {
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
state: "all",
|
||||
state: "open",
|
||||
per_page: 100,
|
||||
}).then((prs) => prs.filter((candidate) =>
|
||||
candidate.base?.repo?.id === context.payload.repository.id &&
|
||||
candidate.head?.sha === targetHeadSha
|
||||
));
|
||||
const openCandidatePRs = candidatePRs.filter((candidate) => candidate.state === "open");
|
||||
if (openCandidatePRs.length > 1) {
|
||||
throw new Error(`ambiguous open PRs from pull list fallback for workflow_run head ${targetHeadSha}: ${openCandidatePRs.length}`);
|
||||
}
|
||||
if (openCandidatePRs.length === 1) {
|
||||
prNumber = openCandidatePRs[0].number;
|
||||
} else if (candidatePRs.length > 0) {
|
||||
core.notice("semantic review skipped: workflow_run target PR is no longer open");
|
||||
core.setOutput("stale", "true");
|
||||
return;
|
||||
} else {
|
||||
if (candidatePRs.length !== 1) {
|
||||
throw new Error(`expected one open PR from pull list fallback for workflow_run head ${targetHeadSha}, got ${candidatePRs.length}`);
|
||||
}
|
||||
prNumber = candidatePRs[0].number;
|
||||
}
|
||||
if (!Number.isInteger(prNumber) || prNumber <= 0) throw new Error("missing pull request binding");
|
||||
const { data: pr } = await github.rest.pulls.get({
|
||||
@@ -363,22 +326,12 @@ jobs:
|
||||
pull_number: prNumber,
|
||||
});
|
||||
if (pr.base.repo.id !== context.payload.repository.id) throw new Error("PR base repo mismatch");
|
||||
if (pr.state !== "open") {
|
||||
core.notice("semantic review skipped: workflow_run target PR is no longer open");
|
||||
core.setOutput("stale", "true");
|
||||
return;
|
||||
}
|
||||
if (!pr.head.repo) {
|
||||
core.notice("semantic review skipped: workflow_run target PR head repository is unavailable");
|
||||
core.setOutput("stale", "true");
|
||||
return;
|
||||
}
|
||||
if (pr.head.sha !== targetHeadSha) {
|
||||
core.notice("semantic review skipped: workflow_run is stale for this PR head");
|
||||
core.setOutput("stale", "true");
|
||||
return;
|
||||
}
|
||||
const baseSha = artifactBaseSha || eventBaseSha || pr.base.sha;
|
||||
const baseSha = eventBaseSha || artifactBaseSha || pr.base.sha;
|
||||
if (!/^[a-f0-9]{40}$/i.test(baseSha)) throw new Error("invalid PR base sha");
|
||||
if ((eventBaseSha || artifactBaseSha) && pr.base.sha !== baseSha) {
|
||||
core.notice("semantic review skipped: workflow_run is stale for this PR base");
|
||||
@@ -430,10 +383,6 @@ jobs:
|
||||
repo: context.repo.repo,
|
||||
pull_number: pr,
|
||||
});
|
||||
if (pull.state !== "open") {
|
||||
core.notice("semantic review skipped infrastructure failure check: PR is no longer open");
|
||||
return;
|
||||
}
|
||||
if (pull.head.sha !== headSha) {
|
||||
core.notice("semantic review skipped infrastructure failure check: PR head changed");
|
||||
return;
|
||||
|
||||
6
.gitignore
vendored
6
.gitignore
vendored
@@ -7,11 +7,6 @@ bin/
|
||||
# Node
|
||||
node_modules/
|
||||
|
||||
# Python (skill-bundled helper scripts)
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
*$py.class
|
||||
|
||||
|
||||
# OS
|
||||
.DS_Store
|
||||
@@ -51,4 +46,3 @@ app.log
|
||||
cover*.out
|
||||
|
||||
lark-env.sh
|
||||
/automations/
|
||||
|
||||
127
CHANGELOG.md
127
CHANGELOG.md
@@ -2,127 +2,6 @@
|
||||
|
||||
All notable changes to this project will be documented in this file.
|
||||
|
||||
## [v1.0.62] - 2026-07-01
|
||||
|
||||
### Features
|
||||
|
||||
- **vc**: Add meeting message send shortcut (#1643)
|
||||
- **doc**: Add document word statistics helper (#1697)
|
||||
- **cli**: Interactive upgrade prompt for bare `lark-cli` invocation (#1498)
|
||||
- **install**: Fail closed when `checksums.txt` is missing during install (#1503)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- **drive**: Improve batch failure handling for push/pull/sync (#1703)
|
||||
- **base**: Support JSON array input for field create (#1661)
|
||||
- **task**: Expose completion state in `my tasks` output (#1641)
|
||||
- **cli**: Reduce public content credential false positives (#1700)
|
||||
|
||||
## [v1.0.61] - 2026-06-30
|
||||
|
||||
### Features
|
||||
|
||||
- **apps**: Add `db`, `file`, `openapi-key` and observability shortcuts (#1596)
|
||||
- **identity**: Add `whoami` command showing effective identity (#1666)
|
||||
- **docs**: Add reference map flags (#1547)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- **identity**: Correct identity diagnosis under external credential providers (#1693)
|
||||
- **cli**: Harden git credential error handling (#1676)
|
||||
|
||||
### Documentation
|
||||
|
||||
- **doc**: Guide document copy skill usage (#1673)
|
||||
- **doc**: Fix lark-doc media token examples (#1662)
|
||||
|
||||
## [v1.0.60] - 2026-06-29
|
||||
|
||||
### Features
|
||||
|
||||
- **affordance**: Per-command usage guidance system with markdown source (#1565)
|
||||
- **event**: Support VC meeting lifecycle events (#1632)
|
||||
- **sheets**: Use `office_sheet_file` parent_type for imported office spreadsheets (#1606)
|
||||
- **authorization**: Expand lark-shared auth guidance and assert clean logout JSON (#1598)
|
||||
- **transport**: Add `LARK_CLI_NO_PROXY_WARN` to silence proxy warning (#1647)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- **install**: Load `@clack/prompts` via dynamic import to avoid `ERR_REQUIRE_ESM` (#1652)
|
||||
|
||||
### Tests
|
||||
|
||||
- **doc**: Derive fetch test flag defaults from `v2FetchFlags` (#1428)
|
||||
|
||||
### Build
|
||||
|
||||
- **ci**: Reduce public content false positives
|
||||
|
||||
## [v1.0.59] - 2026-06-26
|
||||
|
||||
### Features
|
||||
|
||||
- **slides**: Add `+replace-pages` and `xml get` shortcuts, and expose the presentation URL (#1585)
|
||||
- **minutes**: Support speaker list and no-Lark speaker replace (#1594)
|
||||
- **calendar/vc/minutes**: Optimize and extend calendar, vc, minutes, and note shortcuts and skills (#1571)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- **docs**: Hide docs `api-version` compat flag (#1580)
|
||||
|
||||
## [v1.0.58] - 2026-06-25
|
||||
|
||||
### Features
|
||||
|
||||
- **sheets**: Typed table I/O and error contract, workbook import/export, and skill refresh (#1355)
|
||||
- **base**: Add Base URL and title resolve shortcuts (#1338)
|
||||
- **drive**: Add `+member-add` shortcut with wiki space member collection collaborator support (#1204)
|
||||
- **doc**: Support `create` title option (#1536)
|
||||
- **doc**: Add `im-markdown` output format for doc fetch (#1550)
|
||||
- **whiteboard**: Export whiteboard as SVG and update whiteboard via SVG (#1559)
|
||||
- **card**: Support `card.action.trigger` event with auto-fetched card content (#1528)
|
||||
- **task**: Add task event consumer (#1510)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- **doc**: Prefix docs resource shortcuts (#1564)
|
||||
- **binding**: Skip unix mode audit on Windows (#1525)
|
||||
|
||||
### Documentation
|
||||
|
||||
- **approval**: Sync approval skill for meta API commands (#1499)
|
||||
- **doc**: Restore lark-doc style requirements (#1579)
|
||||
- **im**: Document `chat.nickname` get/update/delete (#1378)
|
||||
- **im**: Clarify audio message opus requirement (#1271)
|
||||
|
||||
### Build
|
||||
|
||||
- **ci**: Add public content safeguards and reduce false positives
|
||||
|
||||
## [v1.0.57] - 2026-06-23
|
||||
|
||||
### Features
|
||||
|
||||
- **slides**: Add `+screenshot` to capture slide page images (or render a single `<slide>` XML snippet), returning the local file path instead of Base64 (#1358)
|
||||
- **base**: Support record comments (#1043)
|
||||
- **search**: Surface search API notices (#1413)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- **mail**: Resolve folder/label filter once per `+triage list` call (#1512)
|
||||
- **meta**: Backfill enum value descriptions from options (#1541)
|
||||
- **cli**: Add missing CLI headers for git credential helper (#1539)
|
||||
|
||||
### Documentation
|
||||
|
||||
- **doc**: Refine rich block, path, and block ID guidance (#1508)
|
||||
- **mail**: Trim lark-mail skill context (#1527)
|
||||
- **drive**: Add permission governance workflow guidance (#1292)
|
||||
|
||||
### Build
|
||||
|
||||
- **ci**: Bind semantic review to workflow run head (#1551)
|
||||
|
||||
## [v1.0.56] - 2026-06-18
|
||||
|
||||
### Features
|
||||
@@ -1333,12 +1212,6 @@ Bundled AI agent skills for intelligent assistance:
|
||||
- Bilingual documentation (English & Chinese).
|
||||
- CI/CD pipelines: linting, testing, coverage reporting, and automated releases.
|
||||
|
||||
[v1.0.62]: https://github.com/larksuite/cli/releases/tag/v1.0.62
|
||||
[v1.0.61]: https://github.com/larksuite/cli/releases/tag/v1.0.61
|
||||
[v1.0.60]: https://github.com/larksuite/cli/releases/tag/v1.0.60
|
||||
[v1.0.59]: https://github.com/larksuite/cli/releases/tag/v1.0.59
|
||||
[v1.0.58]: https://github.com/larksuite/cli/releases/tag/v1.0.58
|
||||
[v1.0.57]: https://github.com/larksuite/cli/releases/tag/v1.0.57
|
||||
[v1.0.56]: https://github.com/larksuite/cli/releases/tag/v1.0.56
|
||||
[v1.0.55]: https://github.com/larksuite/cli/releases/tag/v1.0.55
|
||||
[v1.0.54]: https://github.com/larksuite/cli/releases/tag/v1.0.54
|
||||
|
||||
5
Makefile
5
Makefile
@@ -12,7 +12,6 @@ QUALITY_GATE_DIR ?= .tmp/quality-gate
|
||||
QUALITY_GATE_MANIFEST_OUT ?= $(QUALITY_GATE_DIR)/command-manifest.json
|
||||
QUALITY_GATE_COMMAND_INDEX_OUT ?= $(QUALITY_GATE_DIR)/command-index.json
|
||||
QUALITY_GATE_FACTS_OUT ?= $(QUALITY_GATE_DIR)/facts.json
|
||||
PUBLIC_CONTENT_METADATA ?= $(QUALITY_GATE_DIR)/public-content-metadata.json
|
||||
LDFLAGS := -s -w -X $(MODULE)/internal/build.Version=$(VERSION) -X $(MODULE)/internal/build.Date=$(DATE)
|
||||
PREFIX ?= /usr/local
|
||||
|
||||
@@ -70,8 +69,7 @@ integration-test: build
|
||||
test: vet fmt-check script-test unit-test examples-build integration-test
|
||||
|
||||
quality-gate: build
|
||||
mkdir -p $(QUALITY_GATE_DIR) $(dir $(QUALITY_GATE_FACTS_OUT)) $(dir $(PUBLIC_CONTENT_METADATA))
|
||||
test -f $(PUBLIC_CONTENT_METADATA) || printf '{}\n' > $(PUBLIC_CONTENT_METADATA)
|
||||
mkdir -p $(QUALITY_GATE_DIR) $(dir $(QUALITY_GATE_FACTS_OUT))
|
||||
LARKSUITE_CLI_REMOTE_META=off \
|
||||
LARKSUITE_CLI_NO_UPDATE_NOTIFIER=1 \
|
||||
LARKSUITE_CLI_NO_SKILLS_NOTIFIER=1 \
|
||||
@@ -91,7 +89,6 @@ quality-gate: build
|
||||
--changed-from $(QUALITY_GATE_CHANGED_FROM_RESOLVED) \
|
||||
--manifest $(QUALITY_GATE_MANIFEST_OUT) \
|
||||
--command-index $(QUALITY_GATE_COMMAND_INDEX_OUT) \
|
||||
--public-content-metadata $(PUBLIC_CONTENT_METADATA) \
|
||||
--facts-out $(QUALITY_GATE_FACTS_OUT)
|
||||
|
||||
install: build
|
||||
|
||||
@@ -198,7 +198,7 @@ Prefixed with `+`, designed to be friendly for both humans and AI, with smart de
|
||||
```bash
|
||||
lark-cli calendar +agenda
|
||||
lark-cli im +messages-send --chat-id "oc_xxx" --text "Hello"
|
||||
lark-cli docs +create --doc-format markdown --content $'<title>Weekly Report</title>\n# Progress\n- Completed feature X'
|
||||
lark-cli docs +create --api-version v2 --doc-format markdown --content $'<title>Weekly Report</title>\n# Progress\n- Completed feature X'
|
||||
```
|
||||
|
||||
Run `lark-cli <service> --help` to see all shortcut commands.
|
||||
|
||||
@@ -199,7 +199,7 @@ CLI 提供三种粒度的调用方式,覆盖从快速操作到完全自定义
|
||||
```bash
|
||||
lark-cli calendar +agenda
|
||||
lark-cli im +messages-send --chat-id "oc_xxx" --text "Hello"
|
||||
lark-cli docs +create --doc-format markdown --content $'<title>周报</title>\n# 本周进展\n- 完成了 X 功能'
|
||||
lark-cli docs +create --api-version v2 --doc-format markdown --content $'<title>周报</title>\n# 本周进展\n- 完成了 X 功能'
|
||||
```
|
||||
|
||||
运行 `lark-cli <service> --help` 查看所有快捷命令。
|
||||
|
||||
@@ -1,49 +0,0 @@
|
||||
# Affordance
|
||||
|
||||
Per-command usage guidance for the CLI, authored as one markdown file per domain
|
||||
(`<service>.md`). It is surfaced in `lark-cli <command> --help` and in the
|
||||
`schema` output, and read directly at runtime (lazy, cached) — there is no build
|
||||
step. Maintain these files alongside `skills/` and `shortcuts/`.
|
||||
|
||||
## Format
|
||||
|
||||
A small, fixed markdown subset; each file describes one domain:
|
||||
|
||||
# <domain> optional `> skill: <name>` applies to every command below
|
||||
## <command> the command as typed, minus `lark-cli <domain>`
|
||||
<lead paragraph> when to use this command
|
||||
### Avoid when when not to use it / which command to use instead
|
||||
### Prerequisites what you must have first (e.g. an id, and where it comes from)
|
||||
### Tips gotchas and constraints
|
||||
### Examples **description** lines, each followed by a fenced command
|
||||
### <other heading> a custom section; flows through verbatim
|
||||
|
||||
Reference another command with `[[command]]` — it renders as `command` in help.
|
||||
Under `Avoid when` it means "use that one instead"; under `Prerequisites`
|
||||
("… from [[command]]") it means "get the input there first".
|
||||
|
||||
## Example
|
||||
|
||||
## messages get
|
||||
Fetch the full content of a single message by id.
|
||||
|
||||
### Avoid when
|
||||
- Reading several at once → use [[messages batch_get]]
|
||||
|
||||
### Prerequisites
|
||||
- message_id from [[messages list]]
|
||||
|
||||
### Examples
|
||||
|
||||
**Fetch one message**
|
||||
```bash
|
||||
lark-cli mail user_mailbox.messages get --message-id "<id>"
|
||||
```
|
||||
|
||||
## Notes
|
||||
|
||||
- Write plain prose; the only convention is wrapping command references in `[[ ]]`.
|
||||
- Keep it concise and high-signal — don't restate field/flag names, id types, or
|
||||
anything the schema and flags already show; the agent infers the rest.
|
||||
- Command-form headings resolve to method ids via the registry, so plural resource
|
||||
names (`messages`) map to the singular method id (`message`) automatically.
|
||||
@@ -1,19 +0,0 @@
|
||||
# contact
|
||||
> skill: lark-contact
|
||||
|
||||
## user_profiles batch_query
|
||||
Bulk-fetch personal status and signature for user ids you already have.
|
||||
|
||||
### Avoid when
|
||||
- Need more than status/signature (name, dept, email), or don't have the open_id yet → use [[+search-user]]
|
||||
|
||||
### Tips
|
||||
- Off by default — set include_personal_status / include_description to true under query_option
|
||||
- ids in user_ids must match --user-id-type (default open_id)
|
||||
|
||||
### Examples
|
||||
|
||||
**Bulk-query status and signature**
|
||||
```bash
|
||||
lark-cli contact user_profiles batch_query --data '{"user_ids":["ou_3a8b****6a7b"],"query_option":{"include_personal_status":true,"include_description":true}}'
|
||||
```
|
||||
@@ -67,21 +67,8 @@ func NewCmdApiWithContext(ctx context.Context, f *cmdutil.Factory, runF func(*AP
|
||||
|
||||
cmd := &cobra.Command{
|
||||
Use: "api <method> <path>",
|
||||
Short: "Raw HTTP escape hatch — call any endpoint by path (fallback when no typed command exists)",
|
||||
Long: `Raw HTTP escape hatch: send any Lark API request by HTTP method + path.
|
||||
|
||||
Prefer the typed domain command when one exists — it validates parameters,
|
||||
shows the Risk level, gates destructive calls behind --yes, and carries usage
|
||||
guidance that this raw command does not. If a domain command covers your task
|
||||
(browse with ` + "`lark-cli <domain> --help`" + `), use it instead of this.
|
||||
|
||||
Reach for ` + "`api`" + ` only for endpoints that have no typed command yet (e.g.
|
||||
newer/preview APIs), where you already have the HTTP path from the Lark docs.
|
||||
|
||||
Examples:
|
||||
lark-cli api GET /open-apis/calendar/v4/calendars
|
||||
lark-cli api POST /open-apis/im/v1/messages --params '{"receive_id_type":"open_id"}' --data @body.json`,
|
||||
Args: cobra.ExactArgs(2),
|
||||
Short: "Generic Lark API requests",
|
||||
Args: cobra.ExactArgs(2),
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
opts.Method = strings.ToUpper(args[0])
|
||||
opts.Path = args[1]
|
||||
|
||||
@@ -22,11 +22,6 @@ import (
|
||||
|
||||
// NewCmdAuth creates the auth command with subcommands.
|
||||
func NewCmdAuth(f *cmdutil.Factory) *cobra.Command {
|
||||
return NewCmdAuthWithContext(context.Background(), f)
|
||||
}
|
||||
|
||||
// NewCmdAuthWithContext creates the auth command with subcommands.
|
||||
func NewCmdAuthWithContext(ctx context.Context, f *cmdutil.Factory) *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
Use: "auth",
|
||||
Short: "OAuth credentials and authorization management",
|
||||
@@ -43,7 +38,7 @@ func NewCmdAuthWithContext(ctx context.Context, f *cmdutil.Factory) *cobra.Comma
|
||||
}
|
||||
cmdutil.DisableAuthCheck(cmd)
|
||||
|
||||
cmd.AddCommand(NewCmdAuthLoginWithContext(ctx, f, nil))
|
||||
cmd.AddCommand(NewCmdAuthLogin(f, nil))
|
||||
cmd.AddCommand(NewCmdAuthLogout(f, nil))
|
||||
cmd.AddCommand(NewCmdAuthStatus(f, nil))
|
||||
cmd.AddCommand(NewCmdAuthScopes(f, nil))
|
||||
|
||||
@@ -42,11 +42,6 @@ var pollDeviceToken = larkauth.PollDeviceToken
|
||||
|
||||
// NewCmdAuthLogin creates the auth login subcommand.
|
||||
func NewCmdAuthLogin(f *cmdutil.Factory, runF func(*LoginOptions) error) *cobra.Command {
|
||||
return NewCmdAuthLoginWithContext(context.Background(), f, runF)
|
||||
}
|
||||
|
||||
// NewCmdAuthLoginWithContext creates the auth login subcommand.
|
||||
func NewCmdAuthLoginWithContext(ctx context.Context, f *cmdutil.Factory, runF func(*LoginOptions) error) *cobra.Command {
|
||||
opts := &LoginOptions{Factory: f}
|
||||
|
||||
cmd := &cobra.Command{
|
||||
@@ -78,7 +73,7 @@ to generate QR codes (supports ASCII and PNG formats).`,
|
||||
cmd.Flags().StringVar(&opts.Scope, "scope", "", "scopes to request (space- or comma-separated). Combines additively with --domain/--recommend")
|
||||
cmd.Flags().BoolVar(&opts.Recommend, "recommend", false, "request only recommended (auto-approve) scopes")
|
||||
var helpBrand core.LarkBrand
|
||||
if !cmdutil.IsCredentialBootstrapDisabled(ctx) && f != nil && f.Config != nil {
|
||||
if f != nil && f.Config != nil {
|
||||
if cfg, err := f.Config(); err == nil && cfg != nil {
|
||||
helpBrand = cfg.Brand
|
||||
}
|
||||
|
||||
27
cmd/build.go
27
cmd/build.go
@@ -19,7 +19,6 @@ import (
|
||||
"github.com/larksuite/cli/cmd/service"
|
||||
"github.com/larksuite/cli/cmd/skill"
|
||||
cmdupdate "github.com/larksuite/cli/cmd/update"
|
||||
"github.com/larksuite/cli/cmd/whoami"
|
||||
_ "github.com/larksuite/cli/events"
|
||||
"github.com/larksuite/cli/internal/apicatalog"
|
||||
"github.com/larksuite/cli/internal/build"
|
||||
@@ -90,9 +89,8 @@ func WithoutPlugins() BuildOption {
|
||||
}
|
||||
|
||||
// WithoutStrictMode builds the complete repository-owned command tree without
|
||||
// applying user/profile strict-mode pruning or credential-backed bootstrap
|
||||
// probes. It is intended for offline inspection tools and pure local commands
|
||||
// that must not require account configuration.
|
||||
// applying user/profile strict-mode pruning. It is intended for offline
|
||||
// inspection tools, not production execution.
|
||||
func WithoutStrictMode() BuildOption {
|
||||
return func(c *buildConfig) {
|
||||
c.skipStrictMode = true
|
||||
@@ -147,9 +145,6 @@ func buildInternal(ctx context.Context, inv cmdutil.InvocationContext, opts ...B
|
||||
o(cfg)
|
||||
}
|
||||
}
|
||||
if cfg.skipStrictMode {
|
||||
ctx = cmdutil.ContextWithCredentialBootstrapDisabled(ctx)
|
||||
}
|
||||
// Default streams when WithIO is not supplied so the root command's
|
||||
// SetIn/Out/Err calls below don't deref nil. NewDefault also normalizes
|
||||
// partial streams internally; keep both in sync so cfg.streams reflects
|
||||
@@ -175,10 +170,6 @@ func buildInternal(ctx context.Context, inv cmdutil.InvocationContext, opts ...B
|
||||
rootCmd.SetOut(cfg.streams.Out)
|
||||
rootCmd.SetErr(cfg.streams.ErrOut)
|
||||
|
||||
// Root-only usage template (curated Usage synopsis + skills footer); see
|
||||
// rootUsageTemplate.
|
||||
rootCmd.SetUsageTemplate(rootUsageTemplate)
|
||||
|
||||
installTipsHelpFunc(rootCmd)
|
||||
rootCmd.SilenceErrors = true
|
||||
// SilenceUsage as a static field (not only in PersistentPreRun) so it also
|
||||
@@ -196,10 +187,9 @@ func buildInternal(ctx context.Context, inv cmdutil.InvocationContext, opts ...B
|
||||
}
|
||||
|
||||
rootCmd.AddCommand(cmdconfig.NewCmdConfig(f))
|
||||
rootCmd.AddCommand(auth.NewCmdAuthWithContext(ctx, f))
|
||||
rootCmd.AddCommand(auth.NewCmdAuth(f))
|
||||
rootCmd.AddCommand(profile.NewCmdProfile(f))
|
||||
rootCmd.AddCommand(doctor.NewCmdDoctor(f))
|
||||
rootCmd.AddCommand(whoami.NewCmdWhoamiWithContext(ctx, f))
|
||||
rootCmd.AddCommand(api.NewCmdApiWithContext(ctx, f, nil))
|
||||
rootCmd.AddCommand(schema.NewCmdSchema(f, nil))
|
||||
rootCmd.AddCommand(completion.NewCmdCompletion(f))
|
||||
@@ -215,17 +205,10 @@ func buildInternal(ctx context.Context, inv cmdutil.InvocationContext, opts ...B
|
||||
}
|
||||
shortcuts.RegisterShortcutsWithContext(ctx, rootCmd, f)
|
||||
|
||||
groupRootCommands(rootCmd)
|
||||
|
||||
installUnknownSubcommandGuard(rootCmd)
|
||||
// Bare `lark-cli` in an interactive terminal offers an interactive upgrade
|
||||
// before printing help; non-bare invocations and non-TTY are unaffected.
|
||||
installRootUpgradePrompt(f, rootCmd)
|
||||
|
||||
if !cfg.skipStrictMode {
|
||||
if mode := f.ResolveStrictMode(ctx); mode.IsActive() {
|
||||
pruneForStrictMode(rootCmd, mode)
|
||||
}
|
||||
if mode := f.ResolveStrictMode(ctx); mode.IsActive() && !cfg.skipStrictMode {
|
||||
pruneForStrictMode(rootCmd, mode)
|
||||
}
|
||||
|
||||
if cfg.skipPlugins {
|
||||
|
||||
@@ -129,10 +129,7 @@ func doctorRun(opts *DoctorOptions) error {
|
||||
if diagnostics.Bot.Available || diagnostics.User.Available {
|
||||
checks = append(checks, pass("identity_ready", "at least one identity is available"))
|
||||
} else {
|
||||
// No hint: this only summarizes the two checks above, which already carry
|
||||
// the source-appropriate remediation. A command here would be redundant,
|
||||
// or wrong (`auth status` is blocked under an external provider).
|
||||
checks = append(checks, fail("identity_ready", "no usable bot or user identity is available", ""))
|
||||
checks = append(checks, fail("identity_ready", "no usable bot or user identity is available", "run: lark-cli auth status --verify"))
|
||||
}
|
||||
|
||||
// ── 4 & 5. Endpoint reachability ──
|
||||
|
||||
@@ -4,19 +4,14 @@
|
||||
package doctor
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
extcred "github.com/larksuite/cli/extension/credential"
|
||||
"github.com/larksuite/cli/internal/cmdutil"
|
||||
"github.com/larksuite/cli/internal/core"
|
||||
"github.com/larksuite/cli/internal/credential"
|
||||
)
|
||||
|
||||
func TestNewCmdDoctor_FlagParsing(t *testing.T) {
|
||||
@@ -145,84 +140,14 @@ func TestDoctorRun_SplitsBotAndMissingUserIdentity(t *testing.T) {
|
||||
}
|
||||
|
||||
func assertCheck(t *testing.T, checks []checkResult, name, status string) {
|
||||
t.Helper()
|
||||
if got := findCheck(t, checks, name); got.Status != status {
|
||||
t.Fatalf("%s status = %q, want %q", name, got.Status, status)
|
||||
}
|
||||
}
|
||||
|
||||
func findCheck(t *testing.T, checks []checkResult, name string) checkResult {
|
||||
t.Helper()
|
||||
for _, check := range checks {
|
||||
if check.Name == name {
|
||||
return check
|
||||
if check.Status != status {
|
||||
t.Fatalf("%s status = %q, want %q", name, check.Status, status)
|
||||
}
|
||||
return
|
||||
}
|
||||
}
|
||||
t.Fatalf("check %q not found in %#v", name, checks)
|
||||
return checkResult{}
|
||||
}
|
||||
|
||||
type fakeExtProvider struct {
|
||||
name string
|
||||
account *extcred.Account
|
||||
}
|
||||
|
||||
func (p *fakeExtProvider) Name() string { return p.name }
|
||||
func (p *fakeExtProvider) ResolveAccount(context.Context) (*extcred.Account, error) {
|
||||
return p.account, nil
|
||||
}
|
||||
func (p *fakeExtProvider) ResolveToken(context.Context, extcred.TokenSpec) (*extcred.Token, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// Under an external credential provider with no usable identity, the
|
||||
// identity_ready hint must not point at `auth status` (blocked there); the
|
||||
// per-identity checks already carry the source-appropriate escalation.
|
||||
func TestDoctor_ExternalProvider_IdentityReadyHintNotBlockedCommand(t *testing.T) {
|
||||
t.Setenv("LARKSUITE_CLI_CONFIG_DIR", t.TempDir())
|
||||
if err := core.SaveMultiAppConfig(&core.MultiAppConfig{
|
||||
CurrentApp: "default",
|
||||
Apps: []core.AppConfig{{Name: "default", AppId: "cli_x", AppSecret: core.PlainSecret("secret"), Brand: core.BrandFeishu}},
|
||||
}); err != nil {
|
||||
t.Fatalf("SaveMultiAppConfig() error = %v", err)
|
||||
}
|
||||
|
||||
// Provider serves neither identity: bot unsupported, user supported but not
|
||||
// signed in → both unavailable → identity_ready fails.
|
||||
cfg := &core.CliConfig{AppID: "cli_x", Brand: core.BrandFeishu, SupportedIdentities: uint8(extcred.SupportsUser)}
|
||||
cred := credential.NewCredentialProvider(
|
||||
[]extcred.Provider{&fakeExtProvider{name: "corp-sso", account: &extcred.Account{AppID: "cli_x"}}},
|
||||
nil, nil,
|
||||
func() (*http.Client, error) { return nil, nil },
|
||||
)
|
||||
out := &bytes.Buffer{}
|
||||
f := &cmdutil.Factory{
|
||||
Config: func() (*core.CliConfig, error) { return cfg, nil },
|
||||
Credential: cred,
|
||||
IOStreams: &cmdutil.IOStreams{Out: out, ErrOut: &bytes.Buffer{}},
|
||||
}
|
||||
|
||||
if err := doctorRun(&DoctorOptions{Factory: f, Ctx: context.Background(), Offline: true}); err == nil {
|
||||
t.Fatalf("doctorRun() = nil, want failure when no identity is available")
|
||||
}
|
||||
var got struct {
|
||||
Checks []checkResult `json:"checks"`
|
||||
}
|
||||
if err := json.Unmarshal(out.Bytes(), &got); err != nil {
|
||||
t.Fatalf("json.Unmarshal() error = %v\n%s", err, out.String())
|
||||
}
|
||||
|
||||
ready := findCheck(t, got.Checks, "identity_ready")
|
||||
if ready.Status != "fail" {
|
||||
t.Fatalf("identity_ready status = %q, want fail", ready.Status)
|
||||
}
|
||||
// The summary defers to the per-identity checks; it carries no hint of its
|
||||
// own (a command here would be wrong under an external provider).
|
||||
if ready.Hint != "" {
|
||||
t.Fatalf("identity_ready should carry no hint, got %q", ready.Hint)
|
||||
}
|
||||
user := findCheck(t, got.Checks, "user_identity")
|
||||
if !strings.Contains(user.Hint, "external") || strings.Contains(user.Hint, "auth login") {
|
||||
t.Fatalf("user_identity hint not external-appropriate: %q", user.Hint)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,22 +10,10 @@ import (
|
||||
|
||||
"github.com/larksuite/cli/internal/cmdutil"
|
||||
"github.com/larksuite/cli/internal/core"
|
||||
eventlib "github.com/larksuite/cli/internal/event"
|
||||
|
||||
_ "github.com/larksuite/cli/events"
|
||||
)
|
||||
|
||||
func TestEventLookup_VCMeetingLifecycleKeys(t *testing.T) {
|
||||
for _, key := range []string{
|
||||
"vc.meeting.participant_meeting_started_v1",
|
||||
"vc.meeting.participant_meeting_joined_v1",
|
||||
} {
|
||||
if _, ok := eventlib.Lookup(key); !ok {
|
||||
t.Fatalf("event.Lookup(%q) should succeed", key)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestRunList_TextOutput(t *testing.T) {
|
||||
f, stdout, _, _ := cmdutil.TestFactory(t, &core.CliConfig{AppID: "test"})
|
||||
|
||||
@@ -38,9 +26,6 @@ func TestRunList_TextOutput(t *testing.T) {
|
||||
"KEY", "AUTH", "PARAMS", "DESCRIPTION",
|
||||
"im.message.receive_v1",
|
||||
"im.message.message_read_v1",
|
||||
"task.task.update_user_access_v2",
|
||||
"vc.meeting.participant_meeting_started_v1",
|
||||
"vc.meeting.participant_meeting_joined_v1",
|
||||
} {
|
||||
if !strings.Contains(out, want) {
|
||||
t.Errorf("list output missing %q; full output:\n%s", want, out)
|
||||
@@ -70,31 +55,4 @@ func TestRunList_JSONOutput(t *testing.T) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
gotKeys := map[string]map[string]interface{}{}
|
||||
for _, row := range rows {
|
||||
if key, ok := row["key"].(string); ok {
|
||||
gotKeys[key] = row
|
||||
}
|
||||
}
|
||||
var foundTask bool
|
||||
for key, row := range gotKeys {
|
||||
if key == "task.task.update_user_access_v2" {
|
||||
foundTask = true
|
||||
if row["single_consumer"] != true {
|
||||
t.Errorf("task row single_consumer = %v, want true", row["single_consumer"])
|
||||
}
|
||||
}
|
||||
}
|
||||
if !foundTask {
|
||||
t.Fatal("event list JSON missing task.task.update_user_access_v2")
|
||||
}
|
||||
for _, want := range []string{
|
||||
"vc.meeting.participant_meeting_started_v1",
|
||||
"vc.meeting.participant_meeting_joined_v1",
|
||||
} {
|
||||
if _, ok := gotKeys[want]; !ok {
|
||||
t.Errorf("JSON list output missing %q", want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -96,73 +96,6 @@ func TestRunSchema_JSONOutput(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestRunSchema_TaskUpdateUserAccessJSON(t *testing.T) {
|
||||
f, stdout, _, _ := cmdutil.TestFactory(t, &core.CliConfig{AppID: "test"})
|
||||
|
||||
if err := runSchema(f, "task.task.update_user_access_v2", true); err != nil {
|
||||
t.Fatalf("runSchema json: %v", err)
|
||||
}
|
||||
|
||||
var payload map[string]interface{}
|
||||
if err := json.Unmarshal(stdout.Bytes(), &payload); err != nil {
|
||||
t.Fatalf("output is not valid JSON: %v\n%s", err, stdout.String())
|
||||
}
|
||||
if payload["jq_root_path"] != ".event" {
|
||||
t.Errorf("jq_root_path = %v, want .event", payload["jq_root_path"])
|
||||
}
|
||||
if payload["single_consumer"] != true {
|
||||
t.Errorf("single_consumer = %v, want true", payload["single_consumer"])
|
||||
}
|
||||
resolved := payload["resolved_output_schema"].(map[string]interface{})
|
||||
props := resolved["properties"].(map[string]interface{})
|
||||
eventProps := props["event"].(map[string]interface{})["properties"].(map[string]interface{})
|
||||
if got := eventProps["task_guid"].(map[string]interface{})["format"]; got != "task_guid" {
|
||||
t.Errorf("task_guid format = %v, want task_guid", got)
|
||||
}
|
||||
if _, ok := eventProps["event_types"].(map[string]interface{})["items"].(map[string]interface{})["enum"]; !ok {
|
||||
t.Fatalf("event_types enum missing in schema: %#v", eventProps["event_types"])
|
||||
}
|
||||
}
|
||||
|
||||
func TestRunSchema_JSONOutput_VCMeetingLifecycleKeys(t *testing.T) {
|
||||
for _, key := range []string{
|
||||
"vc.meeting.participant_meeting_started_v1",
|
||||
"vc.meeting.participant_meeting_joined_v1",
|
||||
} {
|
||||
t.Run(key, func(t *testing.T) {
|
||||
f, stdout, _, _ := cmdutil.TestFactory(t, &core.CliConfig{AppID: "test"})
|
||||
|
||||
if err := runSchema(f, key, true); err != nil {
|
||||
t.Fatalf("runSchema json: %v", err)
|
||||
}
|
||||
|
||||
var payload map[string]interface{}
|
||||
if err := json.Unmarshal(stdout.Bytes(), &payload); err != nil {
|
||||
t.Fatalf("output is not valid JSON: %v\n%s", err, stdout.String())
|
||||
}
|
||||
if payload["key"] != key {
|
||||
t.Errorf("key = %v, want %s", payload["key"], key)
|
||||
}
|
||||
resolved, ok := payload["resolved_output_schema"].(map[string]interface{})
|
||||
if !ok {
|
||||
t.Fatalf("resolved_output_schema missing or wrong type: %+v", payload)
|
||||
}
|
||||
properties, ok := resolved["properties"].(map[string]interface{})
|
||||
if !ok {
|
||||
t.Fatalf("resolved_output_schema.properties missing or wrong type: %+v", resolved)
|
||||
}
|
||||
for _, field := range []string{"type", "event_id", "timestamp", "meeting_id", "topic", "meeting_no", "start_time", "calendar_event_id"} {
|
||||
if _, ok := properties[field]; !ok {
|
||||
t.Errorf("resolved output schema missing field %q: %+v", field, properties)
|
||||
}
|
||||
}
|
||||
if _, ok := properties["end_time"]; ok {
|
||||
t.Errorf("resolved output schema should not include end_time for %s: %+v", key, properties)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestSchema_RendersSubscriptionKeyMarker(t *testing.T) {
|
||||
const syntheticKey = "test.evt_sub"
|
||||
t.Cleanup(func() { eventlib.UnregisterKeyForTest(syntheticKey) })
|
||||
|
||||
161
cmd/root.go
161
cmd/root.go
@@ -11,11 +11,9 @@ import (
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/larksuite/cli/cmd/service"
|
||||
"github.com/larksuite/cli/errs"
|
||||
"github.com/larksuite/cli/extension/platform"
|
||||
"github.com/larksuite/cli/internal/build"
|
||||
"github.com/larksuite/cli/internal/cmdmeta"
|
||||
"github.com/larksuite/cli/internal/cmdpolicy"
|
||||
"github.com/larksuite/cli/internal/cmdutil"
|
||||
"github.com/larksuite/cli/internal/deprecation"
|
||||
@@ -30,60 +28,43 @@ import (
|
||||
|
||||
const rootLong = `lark-cli — Lark/Feishu CLI tool.
|
||||
|
||||
AGENT QUICKSTART (driving this as an agent? start here):
|
||||
Browse commands: lark-cli <domain> --help # +shortcuts (preferred) and raw API resources
|
||||
Inspect a call: lark-cli schema <service>.<resource>.<method> # params, types, scopes, examples
|
||||
Prefer a +shortcut over the raw API resource when one matches the task.
|
||||
Risk: each command's --help shows read | write | high-risk-write;
|
||||
high-risk-write needs --yes, only after the user confirms.
|
||||
On any API call: --jq <expr> filters JSON output, --dry-run previews the request (runs nothing).
|
||||
USAGE:
|
||||
lark-cli <command> [subcommand] [method] [options]
|
||||
lark-cli api <method> <path> [--params <json>] [--data <json>]
|
||||
lark-cli schema <service.resource.method>
|
||||
|
||||
EXAMPLES (one per command style, in order of preference):
|
||||
lark-cli calendar +agenda # +shortcut — a high-level task, prefer these
|
||||
lark-cli mail user_mailbox.messages list --user-mailbox-id me # typed command for one API method
|
||||
lark-cli schema mail.user_mailbox.messages.list # inspect a method's params before calling
|
||||
lark-cli api GET /open-apis/calendar/v4/calendars # raw escape hatch — any endpoint by HTTP path`
|
||||
EXAMPLES:
|
||||
# View upcoming events
|
||||
lark-cli calendar +agenda
|
||||
|
||||
// rootUsageTemplate is cobra's default usage template with two root-only
|
||||
// additions gated on {{if not .HasParent}}: a curated multi-form Usage synopsis
|
||||
// (replacing cobra's generic "[flags] / [command]") and a human skills-setup
|
||||
// footer. Subcommands render the stock template unchanged. The rest is verbatim
|
||||
// cobra so the command groups and flags are untouched.
|
||||
const rootUsageTemplate = `{{if .HasParent}}Usage:{{if .Runnable}}
|
||||
{{.UseLine}}{{end}}{{if .HasAvailableSubCommands}}
|
||||
{{.CommandPath}} [command]{{end}}{{else}}Usage:
|
||||
lark-cli <command> [subcommand] [method] [flags]
|
||||
lark-cli api <method> <path> [--params <json>] [--data <json>]
|
||||
lark-cli schema <service.resource.method>{{end}}{{if gt (len .Aliases) 0}}
|
||||
# List calendar events
|
||||
lark-cli calendar events instance_view --params '{"calendar_id":"primary","start_time":"1700000000","end_time":"1700086400"}'
|
||||
|
||||
Aliases:
|
||||
{{.NameAndAliases}}{{end}}{{if .HasExample}}
|
||||
# Search users
|
||||
lark-cli contact +search-user --query "John"
|
||||
|
||||
Examples:
|
||||
{{.Example}}{{end}}{{if .HasAvailableSubCommands}}{{$cmds := .Commands}}{{if eq (len .Groups) 0}}
|
||||
# Generic API call
|
||||
lark-cli api GET /open-apis/calendar/v4/calendars
|
||||
|
||||
Available Commands:{{range $cmds}}{{if (or .IsAvailableCommand (eq .Name "help"))}}
|
||||
{{rpad .Name .NamePadding }} {{.Short}}{{end}}{{end}}{{else}}{{range $group := .Groups}}
|
||||
AI AGENT SKILLS:
|
||||
lark-cli pairs with AI agent skills (Claude Code, etc.) that
|
||||
teach the agent Lark API patterns, best practices, and workflows.
|
||||
|
||||
{{.Title}}{{range $cmds}}{{if (and (eq .GroupID $group.ID) (or .IsAvailableCommand (eq .Name "help")))}}
|
||||
{{rpad .Name .NamePadding }} {{.Short}}{{end}}{{end}}{{end}}{{if not .AllChildCommandsHaveGroup}}
|
||||
Install all skills:
|
||||
npx skills add larksuite/cli -g -y
|
||||
|
||||
Additional Commands:{{range $cmds}}{{if (and (eq .GroupID "") (or .IsAvailableCommand (eq .Name "help")))}}
|
||||
{{rpad .Name .NamePadding }} {{.Short}}{{end}}{{end}}{{end}}{{end}}{{end}}{{if .HasAvailableLocalFlags}}
|
||||
Or pick specific domains:
|
||||
npx skills add larksuite/cli -s lark-calendar -y
|
||||
npx skills add larksuite/cli -s lark-im -y
|
||||
|
||||
Flags:
|
||||
{{.LocalFlags.FlagUsages | trimTrailingWhitespaces}}{{end}}{{if .HasAvailableInheritedFlags}}
|
||||
Learn more: https://github.com/larksuite/cli#agent-skills
|
||||
|
||||
Global Flags:
|
||||
{{.InheritedFlags.FlagUsages | trimTrailingWhitespaces}}{{end}}{{if .HasHelpSubCommands}}
|
||||
COMMUNITY:
|
||||
GitHub: https://github.com/larksuite/cli
|
||||
Issues: https://github.com/larksuite/cli/issues
|
||||
Docs: https://open.feishu.cn/document/
|
||||
|
||||
Additional help topics:{{range .Commands}}{{if .IsAdditionalHelpTopicCommand}}
|
||||
{{rpad .CommandPath .CommandPathPadding}} {{.Short}}{{end}}{{end}}{{end}}{{if .HasAvailableSubCommands}}
|
||||
|
||||
Use "{{.CommandPath}} [command] --help" for more information about a command.{{end}}{{if not .HasParent}}
|
||||
|
||||
Skills setup (one-time, humans): npx skills add larksuite/cli -g -y — https://github.com/larksuite/cli#agent-skills{{end}}
|
||||
`
|
||||
More help: lark-cli <command> --help`
|
||||
|
||||
// Execute runs the root command and returns the process exit code.
|
||||
// rawInvocationArgs holds os.Args[1:] captured at Execute() entry. cobra's
|
||||
@@ -103,16 +84,10 @@ func Execute() int {
|
||||
configureFlagCompletions(os.Args)
|
||||
|
||||
ctx := context.Background()
|
||||
buildOpts := []BuildOption{
|
||||
WithIO(os.Stdin, os.Stdout, os.Stderr),
|
||||
HideProfile(isSingleAppMode()),
|
||||
}
|
||||
if isLocalSVGlideInvocation(rawInvocationArgs) {
|
||||
buildOpts = append(buildOpts, WithoutStrictMode())
|
||||
}
|
||||
f, rootCmd, reg := buildInternal(
|
||||
ctx, inv,
|
||||
buildOpts...,
|
||||
WithIO(os.Stdin, os.Stdout, os.Stderr),
|
||||
HideProfile(isSingleAppMode()),
|
||||
)
|
||||
|
||||
// --- Notices (non-blocking) ---
|
||||
@@ -136,30 +111,6 @@ func Execute() int {
|
||||
return 0
|
||||
}
|
||||
|
||||
func isLocalSVGlideInvocation(args []string) bool {
|
||||
positionals := make([]string, 0, 2)
|
||||
for i := 0; i < len(args); i++ {
|
||||
arg := args[i]
|
||||
switch {
|
||||
case arg == "--profile":
|
||||
if i+1 < len(args) {
|
||||
i++
|
||||
}
|
||||
continue
|
||||
case strings.HasPrefix(arg, "--profile="):
|
||||
continue
|
||||
case strings.HasPrefix(arg, "-"):
|
||||
continue
|
||||
default:
|
||||
positionals = append(positionals, arg)
|
||||
if len(positionals) == 2 {
|
||||
return positionals[0] == "slides" && positionals[1] == "+create-svglide"
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// setupNotices wires both the binary update notice and the skills
|
||||
// staleness notice into output.PendingNotice as a composed function.
|
||||
// Each provider populates an independent key under _notice; either
|
||||
@@ -578,49 +529,6 @@ func availableSubcommandNames(cmd *cobra.Command) (available, deprecated []strin
|
||||
return available, deprecated
|
||||
}
|
||||
|
||||
// Root command help groups, so an agent sees content domains, agent tooling, and
|
||||
// CLI management as distinct blocks instead of one flat alphabetical dump.
|
||||
const (
|
||||
groupDomains = "lark-domains"
|
||||
groupTooling = "agent-tooling"
|
||||
groupManagement = "cli-management"
|
||||
)
|
||||
|
||||
// groupRootCommands classifies root's direct children into the help groups,
|
||||
// called once after all commands are registered. Unclassified commands fall to
|
||||
// cobra's "Additional Commands" section.
|
||||
func groupRootCommands(root *cobra.Command) {
|
||||
root.AddGroup(
|
||||
&cobra.Group{ID: groupDomains, Title: "Lark domains:"},
|
||||
&cobra.Group{ID: groupTooling, Title: "Agent tooling:"},
|
||||
&cobra.Group{ID: groupManagement, Title: "CLI management:"},
|
||||
)
|
||||
tooling := map[string]bool{"api": true, "schema": true, "skills": true}
|
||||
management := map[string]bool{"auth": true, "config": true, "profile": true, "doctor": true, "update": true}
|
||||
for _, c := range root.Commands() {
|
||||
if c.GroupID != "" {
|
||||
continue
|
||||
}
|
||||
switch {
|
||||
case tooling[c.Name()]:
|
||||
c.GroupID = groupTooling
|
||||
case management[c.Name()]:
|
||||
c.GroupID = groupManagement
|
||||
case isLarkDomain(c):
|
||||
c.GroupID = groupDomains
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// isLarkDomain reports whether a root child is a Lark domain (service-sourced or
|
||||
// shortcut-tagged), not CLI tooling. Mirrors service.PrepareDomainHelp.
|
||||
func isLarkDomain(c *cobra.Command) bool {
|
||||
if src, _ := cmdmeta.SourceOf(c); src == cmdmeta.SourceService {
|
||||
return true
|
||||
}
|
||||
return cmdmeta.Domain(c) != ""
|
||||
}
|
||||
|
||||
// flagDidYouMean is the root FlagErrorFunc (inherited by all subcommands). It
|
||||
// converts cobra's flag-parse errors into a typed validation envelope: an
|
||||
// unknown flag gets a focused "did you mean" hint (so agents recover even when
|
||||
@@ -702,17 +610,6 @@ func installTipsHelpFunc(root *cobra.Command) {
|
||||
defer func() { f.Hidden = true }()
|
||||
}
|
||||
}
|
||||
// Domain and method commands compose their agent guidance into Long lazily
|
||||
// here (shortcuts attach after service registration); both skip the generic
|
||||
// bottom-of-help append below.
|
||||
if service.PrepareDomainHelp(cmd, embeddedSkillContent) {
|
||||
defaultHelp(cmd, args)
|
||||
return
|
||||
}
|
||||
if service.PrepareMethodHelp(cmd) {
|
||||
defaultHelp(cmd, args)
|
||||
return
|
||||
}
|
||||
defaultHelp(cmd, args)
|
||||
out := cmd.OutOrStdout()
|
||||
if level, ok := cmdutil.GetRisk(cmd); ok {
|
||||
|
||||
@@ -5,12 +5,9 @@ package cmd
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
@@ -29,27 +26,6 @@ import (
|
||||
"github.com/larksuite/cli/internal/registry"
|
||||
)
|
||||
|
||||
type countingKeychain struct {
|
||||
gets int
|
||||
sets int
|
||||
removes int
|
||||
}
|
||||
|
||||
func (k *countingKeychain) Get(service, account string) (string, error) {
|
||||
k.gets++
|
||||
return "", fmt.Errorf("unexpected keychain Get for %s/%s", service, account)
|
||||
}
|
||||
|
||||
func (k *countingKeychain) Set(service, account, value string) error {
|
||||
k.sets++
|
||||
return fmt.Errorf("unexpected keychain Set for %s/%s", service, account)
|
||||
}
|
||||
|
||||
func (k *countingKeychain) Remove(service, account string) error {
|
||||
k.removes++
|
||||
return fmt.Errorf("unexpected keychain Remove for %s/%s", service, account)
|
||||
}
|
||||
|
||||
// TestPersistentPreRunE_AuthCheckDisabledAnnotations verifies that
|
||||
// auth, config, and schema commands have auth check disabled,
|
||||
// while api does not.
|
||||
@@ -99,71 +75,12 @@ func TestPersistentPreRunE_ConfigSubcommands(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestIsLocalSVGlideInvocation(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
args []string
|
||||
want bool
|
||||
}{
|
||||
{name: "local svglide", args: []string{"slides", "+create-svglide", "--action", "init"}, want: true},
|
||||
{name: "with profile", args: []string{"--profile", "demo", "slides", "+create-svglide"}, want: true},
|
||||
{name: "with profile equals", args: []string{"--profile=demo", "slides", "+create-svglide"}, want: true},
|
||||
{name: "other slides shortcut", args: []string{"slides", "+create"}, want: false},
|
||||
{name: "root help", args: []string{"--help"}, want: false},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if got := isLocalSVGlideInvocation(tt.args); got != tt.want {
|
||||
t.Fatalf("isLocalSVGlideInvocation(%v) = %v, want %v", tt.args, got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestLocalSVGlideRootCommandDoesNotTouchKeychain(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
t.Chdir(dir)
|
||||
if err := os.WriteFile("source.md", []byte("# Demo"), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
var in, out, errOut bytes.Buffer
|
||||
kc := &countingKeychain{}
|
||||
_, rootCmd, _ := buildInternal(
|
||||
context.Background(),
|
||||
cmdutil.InvocationContext{},
|
||||
WithIO(&in, &out, &errOut),
|
||||
WithKeychain(kc),
|
||||
WithoutStrictMode(),
|
||||
WithoutPlugins(),
|
||||
)
|
||||
rootCmd.SetArgs([]string{
|
||||
"slides",
|
||||
"+create-svglide",
|
||||
"--action", "init",
|
||||
"--title", "Demo",
|
||||
"--input", "source.md",
|
||||
"--out", "run-demo",
|
||||
})
|
||||
if err := rootCmd.Execute(); err != nil {
|
||||
t.Fatalf("Execute() error = %v\nstdout=%s\nstderr=%s", err, out.String(), errOut.String())
|
||||
}
|
||||
if kc.gets != 0 || kc.sets != 0 || kc.removes != 0 {
|
||||
t.Fatalf("keychain touched: gets=%d sets=%d removes=%d", kc.gets, kc.sets, kc.removes)
|
||||
}
|
||||
if _, err := os.Stat(filepath.Join("run-demo", "run.json")); err != nil {
|
||||
t.Fatalf("missing run.json: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRootLong_AgentSkillsLinkTargetsReadmeSection(t *testing.T) {
|
||||
// The human skills-install guidance now lives in the root usage-template
|
||||
// footer (below the command list), not in the agent-facing Long.
|
||||
if !strings.Contains(rootUsageTemplate, "https://github.com/larksuite/cli#agent-skills") {
|
||||
t.Fatalf("root help footer should link to the README Agent Skills section, got:\n%s", rootUsageTemplate)
|
||||
if !strings.Contains(rootLong, "https://github.com/larksuite/cli#agent-skills") {
|
||||
t.Fatalf("root help should link to the README Agent Skills section, got:\n%s", rootLong)
|
||||
}
|
||||
if strings.Contains(rootUsageTemplate, "https://github.com/larksuite/cli#install-ai-agent-skills") {
|
||||
t.Fatalf("root help should not reference the removed install-ai-agent-skills anchor, got:\n%s", rootUsageTemplate)
|
||||
if strings.Contains(rootLong, "https://github.com/larksuite/cli#install-ai-agent-skills") {
|
||||
t.Fatalf("root help should not reference the removed install-ai-agent-skills anchor, got:\n%s", rootLong)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,90 +0,0 @@
|
||||
// Copyright (c) 2026 Lark Technologies Pte. Ltd.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"fmt"
|
||||
"io"
|
||||
"strings"
|
||||
|
||||
"github.com/larksuite/cli/internal/build"
|
||||
"github.com/larksuite/cli/internal/cmdutil"
|
||||
"github.com/larksuite/cli/internal/update"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
// runRootUpgrade locates the registered `update` subcommand and runs it, so the
|
||||
// interactive root-command upgrade reuses exactly `lark-cli update` behavior
|
||||
// (install-method detection, output, error handling). Package-level var so
|
||||
// tests can stub it and avoid real network / self-update.
|
||||
var runRootUpgrade = func(cmd *cobra.Command) {
|
||||
for _, c := range cmd.Root().Commands() {
|
||||
if c.Name() == "update" && c.RunE != nil {
|
||||
_ = c.RunE(c, nil) // update prints its own output/errors; swallow here
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// isBareRootInvocation reports whether this is a bare `lark-cli` (no subcommand,
|
||||
// no flags) — the only invocation that triggers the interactive upgrade prompt.
|
||||
// Mirrors unknownSubcommandRunE's "bare group prints help" branch: args empty
|
||||
// AND no flag tokens in the raw invocation.
|
||||
func isBareRootInvocation(args []string) bool {
|
||||
return len(args) == 0 && len(flagTokensInArgs(rawInvocationArgs)) == 0
|
||||
}
|
||||
|
||||
// readYes reads one line and reports whether it is an affirmative y/yes.
|
||||
// EOF / empty / anything else → false (default No, matching the [y/N] prompt).
|
||||
func readYes(r io.Reader) bool {
|
||||
line, _ := bufio.NewReader(r).ReadString('\n')
|
||||
switch strings.ToLower(strings.TrimSpace(line)) {
|
||||
case "y", "yes":
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// offerRootUpgrade prompts for an interactive upgrade when running bare
|
||||
// `lark-cli` in an interactive terminal with a cached newer version. Every
|
||||
// failure is swallowed — it must never affect help output or the exit code.
|
||||
func offerRootUpgrade(f *cmdutil.Factory, cmd *cobra.Command) {
|
||||
ios := f.IOStreams
|
||||
// Gates 1/2/3: need to read stdin AND show the prompt on stderr, and require
|
||||
// stdout TTY too so this only fires in a pure foreground terminal session.
|
||||
if !ios.IsTerminal || !ios.OutIsTerminal || !ios.StderrIsTerminal {
|
||||
return
|
||||
}
|
||||
// Gate 4: cached newer version. CheckCached applies opt-out (shouldSkip)
|
||||
// and the IsNewer/semver validation chain; it reads the on-disk cache that
|
||||
// the 24h-throttled RefreshCache maintains (CheckCached itself has no TTL).
|
||||
info := update.CheckCached(build.Version)
|
||||
if info == nil {
|
||||
return
|
||||
}
|
||||
fmt.Fprintf(ios.ErrOut, "lark-cli %s available (current %s). Upgrade now? [y/N]: ", info.Latest, info.Current)
|
||||
if !readYes(ios.In) {
|
||||
return
|
||||
}
|
||||
runRootUpgrade(cmd)
|
||||
}
|
||||
|
||||
// installRootUpgradePrompt wraps the root command's RunE (set to
|
||||
// unknownSubcommandRunE by installUnknownSubcommandGuard) so a bare `lark-cli`
|
||||
// invocation offers an interactive upgrade before printing help. Non-bare
|
||||
// invocations are passed straight through, unchanged.
|
||||
func installRootUpgradePrompt(f *cmdutil.Factory, root *cobra.Command) {
|
||||
inner := root.RunE
|
||||
if inner == nil {
|
||||
return
|
||||
}
|
||||
root.RunE = func(cmd *cobra.Command, args []string) error {
|
||||
if isBareRootInvocation(args) {
|
||||
offerRootUpgrade(f, cmd)
|
||||
}
|
||||
return inner(cmd, args)
|
||||
}
|
||||
}
|
||||
@@ -1,191 +0,0 @@
|
||||
// Copyright (c) 2026 Lark Technologies Pte. Ltd.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/larksuite/cli/internal/build"
|
||||
"github.com/larksuite/cli/internal/cmdutil"
|
||||
"github.com/larksuite/cli/internal/core"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
func writeUpdateState(t *testing.T, dir, latest string) {
|
||||
t.Helper()
|
||||
data := fmt.Sprintf(`{"latest_version":%q,"checked_at":%d}`, latest, time.Now().Unix())
|
||||
if err := os.WriteFile(filepath.Join(dir, "update-state.json"), []byte(data), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestReadYes(t *testing.T) {
|
||||
cases := map[string]bool{
|
||||
"y\n": true, "Y\n": true, "yes\n": true, "YES\n": true, " y \n": true,
|
||||
"n\n": false, "\n": false, "": false, "nope\n": false, "yeah\n": false,
|
||||
}
|
||||
for in, want := range cases {
|
||||
if got := readYes(strings.NewReader(in)); got != want {
|
||||
t.Errorf("readYes(%q) = %v, want %v", in, got, want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestIsBareRootInvocation(t *testing.T) {
|
||||
orig := rawInvocationArgs
|
||||
t.Cleanup(func() { rawInvocationArgs = orig })
|
||||
|
||||
rawInvocationArgs = nil
|
||||
if !isBareRootInvocation([]string{}) {
|
||||
t.Error("empty args + no raw flag tokens should be bare")
|
||||
}
|
||||
rawInvocationArgs = []string{"--profile", "x"}
|
||||
if isBareRootInvocation([]string{}) {
|
||||
t.Error("flag token present → not bare")
|
||||
}
|
||||
rawInvocationArgs = nil
|
||||
if isBareRootInvocation([]string{"im"}) {
|
||||
t.Error("positional arg → not bare")
|
||||
}
|
||||
}
|
||||
|
||||
func TestOfferRootUpgrade(t *testing.T) {
|
||||
origV := build.Version
|
||||
build.Version = "1.0.0" // release version so shouldSkip()==false
|
||||
t.Cleanup(func() { build.Version = origV })
|
||||
|
||||
origRun := runRootUpgrade
|
||||
t.Cleanup(func() { runRootUpgrade = origRun })
|
||||
|
||||
// This test builds a Factory literal (no NewDefault), so it never runs
|
||||
// workspace detection; pin the process-global workspace to Local so
|
||||
// statePath() resolves under LARKSUITE_CLI_CONFIG_DIR rather than a stale
|
||||
// subdir inherited from a prior test in the package.
|
||||
origWS := core.CurrentWorkspace()
|
||||
t.Cleanup(func() { core.SetCurrentWorkspace(origWS) })
|
||||
core.SetCurrentWorkspace(core.WorkspaceLocal)
|
||||
|
||||
cases := []struct {
|
||||
name string
|
||||
in, out, err bool
|
||||
input string
|
||||
latest string // "" → no state file (CheckCached nil)
|
||||
optOut bool
|
||||
wantPrompt, wantRun bool
|
||||
}{
|
||||
{"all-tty+y", true, true, true, "y\n", "2.0.0", false, true, true},
|
||||
{"all-tty+yes", true, true, true, "yes\n", "2.0.0", false, true, true},
|
||||
{"all-tty+n", true, true, true, "n\n", "2.0.0", false, true, false},
|
||||
{"all-tty+empty", true, true, true, "\n", "2.0.0", false, true, false},
|
||||
{"all-tty+eof", true, true, true, "", "2.0.0", false, true, false},
|
||||
{"stdin-not-tty", false, true, true, "y\n", "2.0.0", false, false, false},
|
||||
{"stdout-not-tty", true, false, true, "y\n", "2.0.0", false, false, false},
|
||||
{"stderr-not-tty", true, true, false, "y\n", "2.0.0", false, false, false},
|
||||
{"no-newer-version", true, true, true, "y\n", "", false, false, false},
|
||||
{"already-latest", true, true, true, "y\n", "1.0.0", false, false, false}, // post-upgrade: current == cached latest → no prompt
|
||||
{"cache-older-than-current", true, true, true, "y\n", "0.9.0", false, false, false},
|
||||
{"opt-out", true, true, true, "y\n", "2.0.0", true, false, false},
|
||||
}
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
t.Setenv("LARKSUITE_CLI_CONFIG_DIR", dir)
|
||||
// Clear env that update.shouldSkip treats as "suppress" so the
|
||||
// test is deterministic regardless of host (GitHub Actions sets
|
||||
// CI=true, which would otherwise suppress the prompt).
|
||||
t.Setenv("CI", "")
|
||||
t.Setenv("BUILD_NUMBER", "")
|
||||
t.Setenv("RUN_ID", "")
|
||||
t.Setenv("LARKSUITE_CLI_NO_UPDATE_NOTIFIER", "")
|
||||
if tc.latest != "" {
|
||||
writeUpdateState(t, dir, tc.latest)
|
||||
}
|
||||
if tc.optOut {
|
||||
t.Setenv("LARKSUITE_CLI_NO_UPDATE_NOTIFIER", "1")
|
||||
}
|
||||
called := false
|
||||
runRootUpgrade = func(*cobra.Command) { called = true }
|
||||
|
||||
var errBuf bytes.Buffer
|
||||
f := &cmdutil.Factory{IOStreams: &cmdutil.IOStreams{
|
||||
In: strings.NewReader(tc.input),
|
||||
Out: &bytes.Buffer{},
|
||||
ErrOut: &errBuf,
|
||||
IsTerminal: tc.in,
|
||||
OutIsTerminal: tc.out,
|
||||
StderrIsTerminal: tc.err,
|
||||
}}
|
||||
offerRootUpgrade(f, &cobra.Command{})
|
||||
|
||||
gotPrompt := strings.Contains(errBuf.String(), "available")
|
||||
if gotPrompt != tc.wantPrompt {
|
||||
t.Errorf("prompt: got %v want %v (stderr=%q)", gotPrompt, tc.wantPrompt, errBuf.String())
|
||||
}
|
||||
if called != tc.wantRun {
|
||||
t.Errorf("runRootUpgrade called: got %v want %v", called, tc.wantRun)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestInstallRootUpgradePromptPreservesInner(t *testing.T) {
|
||||
orig := rawInvocationArgs
|
||||
t.Cleanup(func() { rawInvocationArgs = orig })
|
||||
rawInvocationArgs = nil
|
||||
|
||||
innerCalls := 0
|
||||
root := &cobra.Command{Use: "lark-cli"}
|
||||
root.RunE = func(cmd *cobra.Command, args []string) error { innerCalls++; return nil }
|
||||
|
||||
f := &cmdutil.Factory{IOStreams: &cmdutil.IOStreams{
|
||||
In: strings.NewReader(""), Out: &bytes.Buffer{}, ErrOut: &bytes.Buffer{},
|
||||
}}
|
||||
installRootUpgradePrompt(f, root)
|
||||
|
||||
if err := root.RunE(root, []string{}); err != nil {
|
||||
t.Fatalf("bare RunE err = %v", err)
|
||||
}
|
||||
if err := root.RunE(root, []string{"im"}); err != nil {
|
||||
t.Fatalf("non-bare RunE err = %v", err)
|
||||
}
|
||||
if innerCalls != 2 {
|
||||
t.Errorf("inner RunE should run for both bare and non-bare, got %d", innerCalls)
|
||||
}
|
||||
}
|
||||
|
||||
// TestRunRootUpgradeDispatchesToUpdate covers the real runRootUpgrade dispatch
|
||||
// path (not the stub used elsewhere): from any command it must locate the
|
||||
// registered "update" subcommand via cmd.Root() and invoke its RunE.
|
||||
func TestRunRootUpgradeDispatchesToUpdate(t *testing.T) {
|
||||
root := &cobra.Command{Use: "lark-cli"}
|
||||
ran := 0
|
||||
root.AddCommand(&cobra.Command{Use: "update", RunE: func(*cobra.Command, []string) error { ran++; return nil }})
|
||||
child := &cobra.Command{Use: "im"}
|
||||
root.AddCommand(child)
|
||||
|
||||
runRootUpgrade(child) // child.Root() resolves to root, which has "update"
|
||||
|
||||
if ran != 1 {
|
||||
t.Errorf("runRootUpgrade should locate and run update's RunE once, got %d", ran)
|
||||
}
|
||||
}
|
||||
|
||||
// TestInstallRootUpgradePromptNilInnerNoop covers the inner == nil guard:
|
||||
// when root has no RunE, installRootUpgradePrompt must not wrap it.
|
||||
func TestInstallRootUpgradePromptNilInnerNoop(t *testing.T) {
|
||||
root := &cobra.Command{Use: "lark-cli"} // RunE is nil
|
||||
f := &cmdutil.Factory{IOStreams: &cmdutil.IOStreams{
|
||||
In: strings.NewReader(""), Out: &bytes.Buffer{}, ErrOut: &bytes.Buffer{},
|
||||
}}
|
||||
installRootUpgradePrompt(f, root)
|
||||
if root.RunE != nil {
|
||||
t.Error("installRootUpgradePrompt must not wrap a nil RunE (inner==nil guard)")
|
||||
}
|
||||
}
|
||||
@@ -4,211 +4,41 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io/fs"
|
||||
"strings"
|
||||
|
||||
"github.com/larksuite/cli/internal/affordance"
|
||||
"github.com/larksuite/cli/internal/cmdmeta"
|
||||
"github.com/larksuite/cli/internal/cmdutil"
|
||||
"github.com/larksuite/cli/internal/meta"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
// PrepareDomainHelp appends navigational guidance (routing line, risk legend,
|
||||
// skill pointer) to a top-level Lark domain's description, returning false for
|
||||
// anything that is not such a domain. Built lazily at help time because
|
||||
// shortcuts attach after service registration. skillFS (nil-safe) gates the
|
||||
// skill pointer.
|
||||
//
|
||||
// A hand-authored Long is preserved as the base (e.g. event's "Use 'event
|
||||
// consume <EventKey>'…"); service domains carry only a Short at this point, so
|
||||
// we fall back to it. The pristine base is captured once into an annotation so
|
||||
// re-rendering does not append the guidance twice.
|
||||
func PrepareDomainHelp(cmd *cobra.Command, skillFS fs.FS) bool {
|
||||
if cmd.Annotations[schemaPathAnnotation] != "" {
|
||||
return false // a method command
|
||||
}
|
||||
// Direct child of root only — so Domain() reads this command's own tag, and
|
||||
// nested resource groups are excluded.
|
||||
if cmd.Parent() == nil || cmd.Parent().Parent() != nil {
|
||||
return false
|
||||
}
|
||||
// A domain is service-sourced or shortcut-tagged; CLI tooling has neither.
|
||||
if src, _ := cmdmeta.SourceOf(cmd); src != cmdmeta.SourceService && cmdmeta.Domain(cmd) == "" {
|
||||
return false
|
||||
}
|
||||
if !cmd.HasAvailableSubCommands() {
|
||||
return false
|
||||
}
|
||||
|
||||
hasShortcuts, hasResources := false, false
|
||||
for _, c := range cmd.Commands() {
|
||||
if c.Hidden || c.Name() == "help" || c.Name() == "completion" {
|
||||
continue
|
||||
}
|
||||
if strings.HasPrefix(c.Name(), "+") {
|
||||
hasShortcuts = true
|
||||
} else {
|
||||
hasResources = true
|
||||
}
|
||||
}
|
||||
|
||||
var b strings.Builder
|
||||
b.WriteString(domainHelpBase(cmd))
|
||||
if hasShortcuts && hasResources { // routing only matters when both styles exist
|
||||
b.WriteString("\n\nPrefer a +-prefixed shortcut when one matches your task; otherwise use the raw API resource below.")
|
||||
}
|
||||
b.WriteString("\n\nRisk levels (read | write | high-risk-write) appear in each command's --help; high-risk-write requires --yes, only after the user confirms.")
|
||||
if skill := "lark-" + cmd.Name(); skillFS != nil {
|
||||
if _, err := fs.Stat(skillFS, skill+"/SKILL.md"); err == nil {
|
||||
fmt.Fprintf(&b, "\n\nDomain guide (concepts, command choice, conventions): lark-cli skills read %s", skill)
|
||||
}
|
||||
}
|
||||
cmd.Long = b.String()
|
||||
return true
|
||||
}
|
||||
|
||||
// domainHelpBase returns the description to seed domain help with — the
|
||||
// hand-authored Long when present, else the Short — captured once into an
|
||||
// annotation so re-rendering reuses the pristine text instead of the
|
||||
// already-augmented Long.
|
||||
func domainHelpBase(cmd *cobra.Command) string {
|
||||
if base, ok := cmd.Annotations[domainBaseAnnotation]; ok {
|
||||
return base
|
||||
}
|
||||
base := cmd.Long
|
||||
if base == "" {
|
||||
base = cmd.Short
|
||||
}
|
||||
if cmd.Annotations == nil {
|
||||
cmd.Annotations = map[string]string{}
|
||||
}
|
||||
cmd.Annotations[domainBaseAnnotation] = base
|
||||
return base
|
||||
}
|
||||
|
||||
// methodLong is the build-time Long (description + schema pointer +
|
||||
// params-only addendum). Agent guidance is added lazily by PrepareMethodHelp,
|
||||
// so command construction never parses the overlay.
|
||||
func methodLong(description, schemaPath, paramsOnly string) string {
|
||||
// methodLong composes a method command's long help in one place: the
|
||||
// description, the affordance guidance block (when the method has one), the
|
||||
// pointer to the full schema, and the params-only addendum (params whose flag
|
||||
// name is taken — paramFlagBinder.paramsOnlyHelp, "" when none). Affordance
|
||||
// sits near the top so an agent sees when-to-use and few-shot examples before
|
||||
// the flag list.
|
||||
func methodLong(description, affordance, schemaPath, paramsOnly string) string {
|
||||
var b strings.Builder
|
||||
b.WriteString(description)
|
||||
fmt.Fprintf(&b, "\n\nFull parameter schema:\n lark-cli schema %s", schemaPath)
|
||||
if affordance != "" {
|
||||
b.WriteString("\n\n")
|
||||
b.WriteString(affordance)
|
||||
}
|
||||
fmt.Fprintf(&b, "\n\nView parameter definitions before calling:\n lark-cli schema %s", schemaPath)
|
||||
b.WriteString(paramsOnly)
|
||||
return b.String()
|
||||
}
|
||||
|
||||
// Annotation keys PrepareMethodHelp reads to rebuild a method command's Long.
|
||||
const (
|
||||
affordanceServiceAnnotation = "affordance-service"
|
||||
affordanceMethodAnnotation = "affordance-method"
|
||||
schemaPathAnnotation = "method-schema-path"
|
||||
paramsOnlyAnnotation = "method-params-only"
|
||||
domainBaseAnnotation = "affordance-domain-base"
|
||||
)
|
||||
|
||||
// setMethodHelpData records the coordinates PrepareMethodHelp needs (storing a
|
||||
// few strings is the only build-time cost; the overlay stays untouched).
|
||||
func setMethodHelpData(cmd *cobra.Command, service, methodID, schemaPath, paramsOnly string) {
|
||||
if cmd.Annotations == nil {
|
||||
cmd.Annotations = map[string]string{}
|
||||
}
|
||||
if service != "" && methodID != "" {
|
||||
cmd.Annotations[affordanceServiceAnnotation] = service
|
||||
cmd.Annotations[affordanceMethodAnnotation] = methodID
|
||||
}
|
||||
cmd.Annotations[schemaPathAnnotation] = schemaPath
|
||||
if paramsOnly != "" {
|
||||
cmd.Annotations[paramsOnlyAnnotation] = paramsOnly
|
||||
}
|
||||
}
|
||||
|
||||
// PrepareMethodHelp rebuilds a generated method command's Long with the agent
|
||||
// guidance at the TOP (Risk, then the affordance block, then the schema
|
||||
// pointer), returning false for non-method commands. The overlay is parsed
|
||||
// here — only when help is rendered.
|
||||
func PrepareMethodHelp(cmd *cobra.Command) bool {
|
||||
ann := cmd.Annotations
|
||||
if ann == nil {
|
||||
return false
|
||||
}
|
||||
schemaPath, ok := ann[schemaPathAnnotation]
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
|
||||
var b strings.Builder
|
||||
b.WriteString(cmd.Short)
|
||||
if level, ok := cmdutil.GetRisk(cmd); ok {
|
||||
// --yes asserts the USER confirmed; the agent must not self-approve.
|
||||
if level == cmdutil.RiskHighRiskWrite {
|
||||
fmt.Fprintf(&b, "\n\nRisk: %s (requires explicit user confirmation to execute; the agent must NOT add --yes on its own — only pass --yes after the user has confirmed)", level)
|
||||
} else {
|
||||
fmt.Fprintf(&b, "\n\nRisk: %s", level)
|
||||
}
|
||||
}
|
||||
|
||||
var skills []string
|
||||
if raw, ok := affordanceRaw(cmd); ok {
|
||||
if block := renderAffordance(meta.Method{Affordance: raw}); block != "" {
|
||||
b.WriteString("\n\n")
|
||||
b.WriteString(block)
|
||||
}
|
||||
if a, ok := (meta.Method{Affordance: raw}).ParsedAffordance(); ok {
|
||||
skills = a.Skills
|
||||
}
|
||||
}
|
||||
|
||||
fmt.Fprintf(&b, "\n\nFull parameter schema:\n lark-cli schema %s", schemaPath)
|
||||
b.WriteString(ann[paramsOnlyAnnotation])
|
||||
|
||||
if len(skills) > 0 {
|
||||
b.WriteString("\n\nWorkflow skill (end-to-end usage):")
|
||||
for _, s := range skills {
|
||||
fmt.Fprintf(&b, "\n lark-cli skills read %s", s)
|
||||
}
|
||||
}
|
||||
|
||||
cmd.Long = b.String()
|
||||
return true
|
||||
}
|
||||
|
||||
// affordanceLookup is the overlay source; a package var so tests can inject.
|
||||
var affordanceLookup = affordance.For
|
||||
|
||||
// RenderAffordanceForCmd renders a method command's affordance block, or "" when
|
||||
// it carries none.
|
||||
func RenderAffordanceForCmd(cmd *cobra.Command) string {
|
||||
raw, ok := affordanceRaw(cmd)
|
||||
if !ok {
|
||||
return ""
|
||||
}
|
||||
return renderAffordance(meta.Method{Affordance: raw})
|
||||
}
|
||||
|
||||
func affordanceRaw(cmd *cobra.Command) (json.RawMessage, bool) {
|
||||
if cmd.Annotations == nil {
|
||||
return nil, false
|
||||
}
|
||||
service := cmd.Annotations[affordanceServiceAnnotation]
|
||||
methodID := cmd.Annotations[affordanceMethodAnnotation]
|
||||
if service == "" || methodID == "" {
|
||||
return nil, false
|
||||
}
|
||||
return affordanceLookup(service, methodID)
|
||||
}
|
||||
|
||||
// renderAffordance renders a method's affordance as a help block, or "" when it
|
||||
// has none. Sections are joined with blank lines so they scan as distinct groups.
|
||||
// renderAffordance renders a method's affordance as a help block — when to use,
|
||||
// prerequisites, and (most importantly for agents) few-shot Examples — or "" when
|
||||
// the method carries no affordance. It reads the single typed model
|
||||
// (meta.Method.ParsedAffordance) so the help and the envelope agree on shape.
|
||||
func renderAffordance(m meta.Method) string {
|
||||
a, ok := m.ParsedAffordance()
|
||||
if !ok {
|
||||
return ""
|
||||
}
|
||||
|
||||
var sections []string
|
||||
var b strings.Builder
|
||||
bullets := func(title string, items []string) {
|
||||
var nonEmpty []string
|
||||
for _, it := range items {
|
||||
@@ -219,18 +49,15 @@ func renderAffordance(m meta.Method) string {
|
||||
if len(nonEmpty) == 0 {
|
||||
return
|
||||
}
|
||||
var s strings.Builder
|
||||
fmt.Fprintf(&s, "%s:\n", title)
|
||||
fmt.Fprintf(&b, "%s:\n", title)
|
||||
for _, it := range nonEmpty {
|
||||
fmt.Fprintf(&s, " • %s\n", it)
|
||||
fmt.Fprintf(&b, " • %s\n", it)
|
||||
}
|
||||
sections = append(sections, strings.TrimRight(s.String(), "\n"))
|
||||
}
|
||||
|
||||
bullets("When to use", a.UseWhen)
|
||||
bullets("Avoid when", a.AvoidWhen)
|
||||
bullets("Avoid when", a.DoNotUseWhen)
|
||||
bullets("Prerequisites", a.Prerequisites)
|
||||
bullets("Tips", a.Tips)
|
||||
if len(a.Examples) > 0 {
|
||||
var lines []string
|
||||
for _, ex := range a.Examples {
|
||||
@@ -244,13 +71,10 @@ func renderAffordance(m meta.Method) string {
|
||||
}
|
||||
}
|
||||
if len(lines) > 0 {
|
||||
sections = append(sections, "Examples:\n"+strings.Join(lines, "\n"))
|
||||
fmt.Fprintf(&b, "Examples:\n%s\n", strings.Join(lines, "\n"))
|
||||
}
|
||||
}
|
||||
for _, ext := range a.Extensions {
|
||||
bullets(ext.Label, ext.Items)
|
||||
}
|
||||
bullets("Related", a.Related)
|
||||
|
||||
return strings.Join(sections, "\n\n")
|
||||
return strings.TrimRight(b.String(), "\n")
|
||||
}
|
||||
|
||||
@@ -8,18 +8,15 @@ import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/larksuite/cli/internal/cmdmeta"
|
||||
"github.com/larksuite/cli/internal/cmdutil"
|
||||
"github.com/larksuite/cli/internal/meta"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
func TestRenderAffordance(t *testing.T) {
|
||||
raw := json.RawMessage(`{
|
||||
"use_when": ["发送文本消息"],
|
||||
"avoid_when": ["群已解散"],
|
||||
"do_not_use_when": ["群已解散"],
|
||||
"prerequisites": ["已获取 chat_id"],
|
||||
"tips": ["富文本用 msg_type=post"],
|
||||
"examples": [
|
||||
{"description":"发一条文本","command":"lark-cli im messages create --params '{...}'"},
|
||||
{"command":"lark-cli im messages list"},
|
||||
@@ -32,7 +29,6 @@ func TestRenderAffordance(t *testing.T) {
|
||||
"When to use:", "发送文本消息",
|
||||
"Avoid when:", "群已解散",
|
||||
"Prerequisites:", "已获取 chat_id",
|
||||
"Tips:", "富文本用 msg_type=post",
|
||||
"Examples:", "发一条文本", "lark-cli im messages create --params '{...}'",
|
||||
"lark-cli im messages list", // example with no description -> bare command line
|
||||
"Related:", "im.messages.list",
|
||||
@@ -52,12 +48,9 @@ func TestRenderAffordance(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// Affordance is rendered lazily (at --help time) rather than baked into the
|
||||
// command's Long, so building a command never carries the affordance block —
|
||||
// even for a method whose metadata happens to declare one.
|
||||
func TestServiceMethod_AffordanceNotInLong(t *testing.T) {
|
||||
func TestServiceMethod_AffordanceInLong(t *testing.T) {
|
||||
withAff := map[string]interface{}{
|
||||
"id": "messages.create", "path": "messages", "httpMethod": "POST", "description": "发送消息",
|
||||
"path": "messages", "httpMethod": "POST", "description": "发送消息",
|
||||
"affordance": map[string]interface{}{
|
||||
"examples": []interface{}{
|
||||
map[string]interface{}{"description": "发文本", "command": "lark-cli im messages create ..."},
|
||||
@@ -66,120 +59,14 @@ func TestServiceMethod_AffordanceNotInLong(t *testing.T) {
|
||||
}
|
||||
f, _, _, _ := cmdutil.TestFactory(t, testConfig)
|
||||
cmd := NewCmdServiceMethod(f, imSpec(), meta.FromMap(withAff), "create", "messages", nil)
|
||||
if strings.Contains(cmd.Long, "Examples:") {
|
||||
t.Errorf("affordance must not be baked into Long (lazy):\n%s", cmd.Long)
|
||||
if !strings.Contains(cmd.Long, "Examples:") || !strings.Contains(cmd.Long, "lark-cli im messages create ...") {
|
||||
t.Errorf("affordance examples not in command Long:\n%s", cmd.Long)
|
||||
}
|
||||
// The lookup ref is recorded so the help path can resolve it later.
|
||||
if cmd.Annotations[affordanceServiceAnnotation] != "im" || cmd.Annotations[affordanceMethodAnnotation] != "messages.create" {
|
||||
t.Errorf("affordance ref annotations = %v, want im/messages.create", cmd.Annotations)
|
||||
}
|
||||
}
|
||||
|
||||
// RenderAffordanceForCmd resolves a command's overlay through the (injectable)
|
||||
// lookup and renders it; commands without a ref render nothing.
|
||||
func TestRenderAffordanceForCmd(t *testing.T) {
|
||||
orig := affordanceLookup
|
||||
t.Cleanup(func() { affordanceLookup = orig })
|
||||
affordanceLookup = func(service, methodID string) (json.RawMessage, bool) {
|
||||
if service != "im" || methodID != "messages.create" {
|
||||
return nil, false
|
||||
}
|
||||
return json.RawMessage(`{"use_when":["发文本消息"],"tips":["富文本用 msg_type=post"],"examples":[{"description":"发一条","command":"lark-cli im messages create ..."}]}`), true
|
||||
}
|
||||
|
||||
f, _, _, _ := cmdutil.TestFactory(t, testConfig)
|
||||
withRef := map[string]interface{}{"id": "messages.create", "path": "messages", "httpMethod": "POST", "description": "发送消息"}
|
||||
cmd := NewCmdServiceMethod(f, imSpec(), meta.FromMap(withRef), "create", "messages", nil)
|
||||
block := RenderAffordanceForCmd(cmd)
|
||||
for _, want := range []string{"When to use:", "发文本消息", "Tips:", "富文本用 msg_type=post", "Examples:", "lark-cli im messages create ..."} {
|
||||
if !strings.Contains(block, want) {
|
||||
t.Errorf("RenderAffordanceForCmd missing %q in:\n%s", want, block)
|
||||
}
|
||||
}
|
||||
|
||||
// No overlay for this method id -> empty block.
|
||||
noRef := map[string]interface{}{"id": "x.list", "path": "x", "httpMethod": "GET", "description": "d"}
|
||||
cmd2 := NewCmdServiceMethod(f, imSpec(), meta.FromMap(noRef), "list", "x", nil)
|
||||
if got := RenderAffordanceForCmd(cmd2); got != "" {
|
||||
t.Errorf("method with no overlay should render nothing, got:\n%s", got)
|
||||
}
|
||||
}
|
||||
|
||||
// PrepareMethodHelp composes the guidance into Long at the top: description,
|
||||
// then the affordance block, then the full-schema pointer — so an agent reads
|
||||
// when-to-use/examples before the flag list.
|
||||
func TestPrepareMethodHelp(t *testing.T) {
|
||||
orig := affordanceLookup
|
||||
t.Cleanup(func() { affordanceLookup = orig })
|
||||
affordanceLookup = func(_, _ string) (json.RawMessage, bool) {
|
||||
return json.RawMessage(`{"use_when":["发文本消息"],"examples":[{"description":"发一条","command":"lark-cli im messages create ..."}]}`), true
|
||||
}
|
||||
|
||||
f, _, _, _ := cmdutil.TestFactory(t, testConfig)
|
||||
m := map[string]interface{}{"id": "messages.create", "path": "messages", "httpMethod": "POST", "description": "发送消息"}
|
||||
cmd := NewCmdServiceMethod(f, imSpec(), meta.FromMap(m), "create", "messages", nil)
|
||||
|
||||
if !PrepareMethodHelp(cmd) {
|
||||
t.Fatal("PrepareMethodHelp returned false for a service-method command")
|
||||
}
|
||||
long := cmd.Long
|
||||
// Description leads; affordance block sits above the schema pointer.
|
||||
descAt := strings.Index(long, "发送消息")
|
||||
useAt := strings.Index(long, "When to use:")
|
||||
exAt := strings.Index(long, "Examples:")
|
||||
schemaAt := strings.Index(long, "Full parameter schema:")
|
||||
if descAt != 0 {
|
||||
t.Errorf("description should lead Long, got:\n%s", long)
|
||||
}
|
||||
if !(descAt < useAt && useAt < exAt && exAt < schemaAt) {
|
||||
t.Errorf("order should be description < affordance < schema pointer; got desc=%d use=%d ex=%d schema=%d\n%s", descAt, useAt, exAt, schemaAt, long)
|
||||
}
|
||||
|
||||
// A non-service command (no schema-path annotation) is left untouched.
|
||||
if PrepareMethodHelp(&cobra.Command{Use: "plain"}) {
|
||||
t.Error("PrepareMethodHelp should return false for a non-service command")
|
||||
}
|
||||
}
|
||||
|
||||
// domainCmd wires a domain-tagged command with a subcommand under a root, the
|
||||
// shape PrepareDomainHelp expects.
|
||||
func domainCmd(short, long string) *cobra.Command {
|
||||
root := &cobra.Command{Use: "root"}
|
||||
dom := &cobra.Command{Use: "event", Short: short, Long: long}
|
||||
cmdmeta.SetDomain(dom, "event")
|
||||
dom.AddCommand(&cobra.Command{Use: "consume", Run: func(*cobra.Command, []string) {}})
|
||||
root.AddCommand(dom)
|
||||
return dom
|
||||
}
|
||||
|
||||
func TestPrepareDomainHelp_PreservesHandAuthoredLong(t *testing.T) {
|
||||
const long = "Unified event consumption system. Use 'event consume <EventKey>'."
|
||||
dom := domainCmd("Consume and manage real-time events", long)
|
||||
|
||||
if !PrepareDomainHelp(dom, nil) {
|
||||
t.Fatal("PrepareDomainHelp returned false for a domain-tagged command")
|
||||
}
|
||||
if !strings.HasPrefix(dom.Long, long) {
|
||||
t.Errorf("hand-authored Long must lead; got:\n%s", dom.Long)
|
||||
}
|
||||
if !strings.Contains(dom.Long, "Risk levels") {
|
||||
t.Errorf("domain guidance should be appended; got:\n%s", dom.Long)
|
||||
}
|
||||
|
||||
// Re-rendering must not append the guidance a second time.
|
||||
PrepareDomainHelp(dom, nil)
|
||||
if n := strings.Count(dom.Long, "Risk levels"); n != 1 {
|
||||
t.Errorf("guidance appended %d times across re-renders, want 1:\n%s", n, dom.Long)
|
||||
}
|
||||
}
|
||||
|
||||
// A service domain carries only a Short at help time; it seeds the base.
|
||||
func TestPrepareDomainHelp_FallsBackToShort(t *testing.T) {
|
||||
dom := domainCmd("Message and group chat management", "")
|
||||
if !PrepareDomainHelp(dom, nil) {
|
||||
t.Fatal("PrepareDomainHelp returned false for a domain-tagged command")
|
||||
}
|
||||
if !strings.HasPrefix(dom.Long, "Message and group chat management") {
|
||||
t.Errorf("Short should seed Long when no hand-authored Long exists; got:\n%s", dom.Long)
|
||||
|
||||
// A method with no affordance adds no guidance block.
|
||||
plain := map[string]interface{}{"path": "x", "httpMethod": "GET", "description": "d"}
|
||||
cmd2 := NewCmdServiceMethod(f, imSpec(), meta.FromMap(plain), "list", "x", nil)
|
||||
if strings.Contains(cmd2.Long, "Examples:") {
|
||||
t.Errorf("no-affordance method should have no Examples in Long:\n%s", cmd2.Long)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -60,11 +60,8 @@ func TestServiceFlagGroups_AgentContract(t *testing.T) {
|
||||
if i := idx("--chat-id"); i < iParams || i > iBody {
|
||||
t.Errorf("--chat-id not under API Parameters:\n%s", out)
|
||||
}
|
||||
// The redundant "<name>, required|optional." prefix is gone: required-ness is
|
||||
// carried by the Required:/Optional: subheadings, and the snake-case --params
|
||||
// key by the schema envelope — so it isn't echoed on every flag line.
|
||||
if strings.Contains(out, "chat_id, required") || strings.Contains(out, "member_id_type, optional") {
|
||||
t.Errorf("redundant <name>, required/optional prefix should not appear:\n%s", out)
|
||||
if !strings.Contains(out, "chat_id, required") {
|
||||
t.Errorf("typed flag help format wrong:\n%s", out)
|
||||
}
|
||||
if !strings.Contains(out, "enum: open_id=以 open_id 标识用户|user_id=以 user_id 标识用户") {
|
||||
t.Errorf("expected compact enum value=meaning inline:\n%s", out)
|
||||
|
||||
@@ -30,11 +30,6 @@ func fieldFacts(f meta.Field) []string {
|
||||
if d := sanitizeFieldDesc(f.Description); d != "" {
|
||||
facts = append(facts, d)
|
||||
}
|
||||
if f.CanonicalType() == "boolean" {
|
||||
// cobra shows no type word for bools and swallows a separate value as a
|
||||
// positional, so spell out the presence-only contract.
|
||||
facts = append(facts, "bool flag (presence = true; omit for false; takes no value)")
|
||||
}
|
||||
if opts := f.EnumOptions(); len(opts) > 0 {
|
||||
facts = append(facts, "enum: "+formatEnumInline(opts))
|
||||
}
|
||||
@@ -47,15 +42,20 @@ func fieldFacts(f meta.Field) []string {
|
||||
return facts
|
||||
}
|
||||
|
||||
// paramFlagUsage renders the typed param flag's help line: the field's facts
|
||||
// joined inline. Required/optional is not repeated here — the grouped help's
|
||||
// Required:/Optional: subheadings already partition the flags — and the
|
||||
// snake-case --params key is carried by the schema envelope (each param's
|
||||
// property + "flag") and the params-only addendum, so it isn't echoed on every
|
||||
// line either. Returns "" when the field has no facts (cobra then shows the bare
|
||||
// flag with its type).
|
||||
// paramFlagUsage renders the typed param flag's help line:
|
||||
//
|
||||
// <param_name>, required|optional[. <fact>]...
|
||||
//
|
||||
// It leads with the canonical underscore param name (the key this flag
|
||||
// overrides in --params) and required/optional, then joins the field's facts
|
||||
// inline.
|
||||
func paramFlagUsage(f meta.Field) string {
|
||||
return strings.Join(fieldFacts(f), ". ")
|
||||
req := "optional"
|
||||
if f.Required {
|
||||
req = "required"
|
||||
}
|
||||
parts := append([]string{fmt.Sprintf("%s, %s", f.Name, req)}, fieldFacts(f)...)
|
||||
return strings.Join(parts, ". ") + "."
|
||||
}
|
||||
|
||||
// paramExample picks a concrete sample for a params-only field's --help snippet:
|
||||
@@ -103,23 +103,8 @@ func sanitizeOptionDesc(s string) string { return inlineClause(s, "。;;\n\r",
|
||||
// sanitizeFieldDesc is the field-description policy: one line per field, so
|
||||
// keep full sentences and cut only at note separators (meta_data appends
|
||||
// bullet notes after ;/;) — the later sentence often carries the key
|
||||
// affordance, e.g. user_mailbox_id's `可以输入"me"`. The trailing doc
|
||||
// cross-reference is dropped first (see cutDocRef).
|
||||
func sanitizeFieldDesc(s string) string { return inlineClause(cutDocRef(s), ";;\n\r", 60) }
|
||||
|
||||
// docRefRe matches a "see the docs" breadcrumb (更多信息参见…/获取方式见…/详见…).
|
||||
// On the compact flag line the markdown link's URL is stripped, so the
|
||||
// breadcrumb is a dead pointer — drop it. Anchored on a leading clause separator
|
||||
// so a subject that runs straight into the phrase isn't orphaned.
|
||||
var docRefRe = regexp.MustCompile(`[。;;,,、]\s*(更多信息|获取方式|获取方法|详见|[请可]?参[见考阅])`)
|
||||
|
||||
// cutDocRef truncates s at the first doc-reference breadcrumb.
|
||||
func cutDocRef(s string) string {
|
||||
if loc := docRefRe.FindStringIndex(s); loc != nil {
|
||||
return s[:loc[0]]
|
||||
}
|
||||
return s
|
||||
}
|
||||
// affordance, e.g. user_mailbox_id's `可以输入"me"`.
|
||||
func sanitizeFieldDesc(s string) string { return inlineClause(s, ";;\n\r", 60) }
|
||||
|
||||
// formatEnumInline renders allowed values for the help line: "v=meaning" when
|
||||
// the value carries a (sanitized, truncated) description — so opaque numeric
|
||||
|
||||
@@ -7,7 +7,6 @@ import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/larksuite/cli/errs"
|
||||
@@ -65,38 +64,15 @@ func registerServiceWithContext(ctx context.Context, parent *cobra.Command, svc
|
||||
// resource-command chain — one level for a flat dotted resource like
|
||||
// "chat.members", deeper for genuinely nested resources. A service with no
|
||||
// methods keeps its bare command (svcCmd is created above regardless).
|
||||
refs := apicatalog.ServiceMethods(svc, nil)
|
||||
|
||||
// Collect each resource's verbs up front so resourceShort can summarize a
|
||||
// resource as its verb list from the first ensureChildCommand call.
|
||||
verbs := map[string][]string{}
|
||||
for _, ref := range refs {
|
||||
key := strings.Join(ref.ResourcePath, ".")
|
||||
verbs[key] = append(verbs[key], ref.Method.Name)
|
||||
}
|
||||
|
||||
for _, ref := range refs {
|
||||
for _, ref := range apicatalog.ServiceMethods(svc, nil) {
|
||||
resCmd := svcCmd
|
||||
var path []string
|
||||
for _, seg := range ref.ResourcePath {
|
||||
path = append(path, seg)
|
||||
resCmd = ensureChildCommand(resCmd, seg, resourceShort(seg, verbs[strings.Join(path, ".")]))
|
||||
resCmd = ensureChildCommand(resCmd, seg, seg+" operations")
|
||||
}
|
||||
resCmd.AddCommand(buildMethodCommand(ctx, f, newMethodCommandSpec(ref), nil, parent.PersistentFlags()))
|
||||
}
|
||||
}
|
||||
|
||||
// resourceShort summarizes a resource as its sorted verb list, or the
|
||||
// "<name> operations" placeholder for an intermediate group with no methods.
|
||||
func resourceShort(seg string, verbs []string) string {
|
||||
if len(verbs) == 0 {
|
||||
return seg + " operations"
|
||||
}
|
||||
sorted := append([]string(nil), verbs...)
|
||||
sort.Strings(sorted)
|
||||
return strings.Join(sorted, ", ")
|
||||
}
|
||||
|
||||
// serviceShort is the service command's help summary: the localized description
|
||||
// from the registry, falling back to the metadata's own description.
|
||||
func serviceShort(svc meta.Service) string {
|
||||
@@ -201,19 +177,7 @@ type methodCommandSpec struct {
|
||||
// the API declares a body.
|
||||
acceptsBody bool
|
||||
declaresBody bool
|
||||
paginates bool // method accepts a page_token param (so --page-all is meaningful)
|
||||
serviceName string // owning service name (e.g. "approval"), for the lazy affordance lookup
|
||||
}
|
||||
|
||||
// methodPaginates reports whether a method takes a page_token param, the signal
|
||||
// that makes the --page-all/--page-limit/--page-delay flags meaningful.
|
||||
func methodPaginates(m meta.Method) bool {
|
||||
for _, f := range m.Params() {
|
||||
if f.Name == "page_token" {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
affordance string // rendered hand-authored usage guidance (when-to-use, examples); "" if none
|
||||
}
|
||||
|
||||
func newMethodCommandSpec(ref apicatalog.MethodRef) methodCommandSpec {
|
||||
@@ -222,7 +186,6 @@ func newMethodCommandSpec(ref apicatalog.MethodRef) methodCommandSpec {
|
||||
method: m,
|
||||
schemaPath: ref.SchemaPath(),
|
||||
servicePath: ref.Service.ServicePath,
|
||||
serviceName: ref.Service.Name,
|
||||
risk: m.Risk,
|
||||
restricts: m.RestrictsIdentity(),
|
||||
identities: m.Identities(),
|
||||
@@ -230,7 +193,7 @@ func newMethodCommandSpec(ref apicatalog.MethodRef) methodCommandSpec {
|
||||
fileFields: detectFileFields(m),
|
||||
acceptsBody: methodTakesBody(m.HTTPMethod),
|
||||
declaresBody: len(m.Data()) > 0 || len(m.Files()) > 0,
|
||||
paginates: methodPaginates(m),
|
||||
affordance: renderAffordance(m),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -291,14 +254,6 @@ func buildMethodCommand(ctx context.Context, f *cmdutil.Factory, spec methodComm
|
||||
cmd.Flags().BoolVar(&opts.PageAll, "page-all", false, "automatically paginate through all pages")
|
||||
cmd.Flags().IntVar(&opts.PageLimit, "page-limit", 10, "max pages to fetch with --page-all (0 = unlimited)")
|
||||
cmd.Flags().IntVar(&opts.PageDelay, "page-delay", 200, "delay in ms between pages")
|
||||
// Keep the pagination flags registered (a harmless no-op if passed) but hide
|
||||
// them from help on non-paginating commands, so help doesn't imply a
|
||||
// get/write can paginate.
|
||||
if !spec.paginates {
|
||||
for _, name := range []string{"page-all", "page-limit", "page-delay"} {
|
||||
_ = cmd.Flags().MarkHidden(name)
|
||||
}
|
||||
}
|
||||
cmd.Flags().StringVar(&opts.Format, "format", "json", "output format: json|ndjson|table|csv")
|
||||
cmd.Flags().Bool("json", false, "shorthand for --format json")
|
||||
cmd.Flags().StringVarP(&opts.JqExpr, "jq", "q", "", "jq expression to filter JSON output")
|
||||
@@ -316,11 +271,10 @@ func buildMethodCommand(ctx context.Context, f *cmdutil.Factory, spec methodComm
|
||||
|
||||
// Registered last so the collision guard sees the standard flags above.
|
||||
opts.binder = newParamFlagBinder(cmd, spec.params, reserved)
|
||||
// Build-time Long; the agent guidance is added lazily by PrepareMethodHelp
|
||||
// (setMethodHelpData records the coordinates it needs).
|
||||
paramsOnly := opts.binder.paramsOnlyHelp()
|
||||
cmd.Long = methodLong(m.Description, spec.schemaPath, paramsOnly)
|
||||
setMethodHelpData(cmd, spec.serviceName, m.ID, spec.schemaPath, paramsOnly)
|
||||
// Single composition point for Long: description, affordance, schema
|
||||
// pointer, and the binder's params-only addendum (params whose flag name is
|
||||
// taken, reachable via --params only).
|
||||
cmd.Long = methodLong(m.Description, spec.affordance, spec.schemaPath, opts.binder.paramsOnlyHelp())
|
||||
|
||||
// Group flags for the grouped --help renderer (typed param flags are grouped
|
||||
// as API Parameters by the binder). tagFlagGroup is a no-op for flags not
|
||||
@@ -338,11 +292,13 @@ func buildMethodCommand(ctx context.Context, f *cmdutil.Factory, spec methodComm
|
||||
tagFlagGroup(cmd.Flags(), "file", groupBody)
|
||||
if fl := cmd.Flags().Lookup("params"); fl != nil {
|
||||
annotate(fl, flagGroupAnnotation, []string{groupRaw})
|
||||
// Keep the precedence rule on the flag's own one line (not a multi-line
|
||||
// note that breaks the one-entry-per-flag rhythm an agent parses). Only
|
||||
// meaningful when typed flags exist to override.
|
||||
// State the precedence rule where the agent reads it: --params is the
|
||||
// base, typed flags override. Only meaningful when typed flags exist.
|
||||
if len(spec.params) > 0 {
|
||||
fl.Usage = "Raw URL/query params JSON. Supports - and @file. If both set, typed flags override matching keys in --params."
|
||||
annotate(fl, flagNoteAnnotation, []string{
|
||||
"Typed API parameter flags above are preferred.",
|
||||
"If both are set, typed flags override matching keys in --params.",
|
||||
})
|
||||
}
|
||||
}
|
||||
for _, name := range []string{"as", "dry-run", "page-all", "page-limit", "page-delay", "yes"} {
|
||||
|
||||
@@ -1,169 +0,0 @@
|
||||
// Copyright (c) 2026 Lark Technologies Pte. Ltd.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package whoami
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/larksuite/cli/internal/cmdutil"
|
||||
"github.com/larksuite/cli/internal/core"
|
||||
"github.com/larksuite/cli/internal/identitydiag"
|
||||
"github.com/larksuite/cli/internal/output"
|
||||
)
|
||||
|
||||
// whoamiResult is the structured output of `lark-cli whoami`.
|
||||
//
|
||||
// The self-vs-delegated distinction is carried by `identity`: a bot identity is
|
||||
// the app acting as itself; a user identity is the app acting *on behalf of* a
|
||||
// person (calls are attributed to that user, who is not necessarily present).
|
||||
// onBehalfOf only *names* that person and so appears only once a user is
|
||||
// resolved — a user identity that is not signed in still has identity "user"
|
||||
// but no onBehalfOf yet. Do not read "no onBehalfOf" as "self"; read `identity`.
|
||||
type whoamiResult struct {
|
||||
Profile string `json:"profile"`
|
||||
AppID string `json:"appId"`
|
||||
Brand core.LarkBrand `json:"brand"`
|
||||
DefaultAs string `json:"defaultAs"`
|
||||
Identity string `json:"identity"`
|
||||
IdentitySource string `json:"identitySource"`
|
||||
Available bool `json:"available"`
|
||||
TokenStatus string `json:"tokenStatus"`
|
||||
OnBehalfOf *delegatedUser `json:"onBehalfOf,omitempty"`
|
||||
Hint string `json:"hint,omitempty"`
|
||||
}
|
||||
|
||||
// delegatedUser is the user a user-identity acts on behalf of.
|
||||
type delegatedUser struct {
|
||||
UserName string `json:"userName,omitempty"`
|
||||
OpenID string `json:"openId,omitempty"`
|
||||
}
|
||||
|
||||
// Options holds inputs for the whoami command.
|
||||
type Options struct {
|
||||
Factory *cmdutil.Factory
|
||||
As string
|
||||
}
|
||||
|
||||
// NewCmdWhoami creates the top-level whoami command. It reports the identity
|
||||
// that the next API call would actually use (resolved via Factory.ResolveAs),
|
||||
// together with the active profile, app, and token status. Output is always
|
||||
// JSON — whoami is consumed by agents. With the built-in credential path it is
|
||||
// local-only; when an external credential provider manages tokens, resolving
|
||||
// the identity may contact that provider.
|
||||
func NewCmdWhoami(f *cmdutil.Factory) *cobra.Command {
|
||||
return NewCmdWhoamiWithContext(context.Background(), f)
|
||||
}
|
||||
|
||||
// NewCmdWhoamiWithContext creates the whoami command using the build context
|
||||
// for registration-time strict-mode presentation.
|
||||
func NewCmdWhoamiWithContext(ctx context.Context, f *cmdutil.Factory) *cobra.Command {
|
||||
opts := &Options{Factory: f}
|
||||
cmd := &cobra.Command{
|
||||
Use: "whoami",
|
||||
Short: "Show the current effective identity, app, profile, and token status (JSON)",
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
return whoamiRun(cmd, opts)
|
||||
},
|
||||
}
|
||||
cmdutil.DisableAuthCheck(cmd)
|
||||
cmdutil.AddAPIIdentityFlag(ctx, cmd, f, &opts.As)
|
||||
// Output is always JSON. Accept (and ignore) --json so existing
|
||||
// `whoami --json` callers don't break; hide it to avoid implying a non-JSON
|
||||
// mode exists.
|
||||
cmd.Flags().Bool("json", true, "deprecated: output is always JSON")
|
||||
_ = cmd.Flags().MarkHidden("json")
|
||||
cmdutil.SetRisk(cmd, "read")
|
||||
return cmd
|
||||
}
|
||||
|
||||
func whoamiRun(cmd *cobra.Command, opts *Options) error {
|
||||
f := opts.Factory
|
||||
cfg, err := f.Config()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
ctx := cmd.Context()
|
||||
flagAs := core.Identity(opts.As)
|
||||
as := f.ResolveAs(ctx, cmd, flagAs)
|
||||
// Validate as a real API call does (strict mode, then identity) so whoami
|
||||
// can't preview an identity the next call would refuse.
|
||||
if err := f.CheckStrictMode(ctx, as); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := f.CheckIdentity(as, []string{"user", "bot"}); err != nil {
|
||||
return err
|
||||
}
|
||||
source := resolveSource(
|
||||
cmd.Flags().Changed("as"),
|
||||
flagAs,
|
||||
f.IdentityAutoDetected,
|
||||
f.ResolveStrictMode(ctx).ForcedIdentity(),
|
||||
)
|
||||
diag := identitydiag.Diagnose(ctx, f, cfg, false)
|
||||
res := buildResult(cfg, as, source, diag)
|
||||
output.PrintJson(f.IOStreams.Out, res)
|
||||
return nil
|
||||
}
|
||||
|
||||
// resolveSource derives how the effective identity became effective.
|
||||
// Mirrors Factory.ResolveAs precedence: explicit flag wins; otherwise an
|
||||
// auto-detected result means auto-detect; otherwise a strict-mode forced
|
||||
// identity means strict-mode; otherwise it came from configured default-as.
|
||||
// Values are snake_case to match the other enum fields (e.g. tokenStatus).
|
||||
func resolveSource(changedAs bool, flagAs core.Identity, autoDetected bool, strictForced core.Identity) string {
|
||||
if changedAs && (flagAs == core.AsUser || flagAs == core.AsBot) {
|
||||
return "flag"
|
||||
}
|
||||
if autoDetected {
|
||||
return "auto_detect"
|
||||
}
|
||||
if strictForced != "" {
|
||||
return "strict_mode"
|
||||
}
|
||||
return "default_as"
|
||||
}
|
||||
|
||||
// buildResult maps the resolved identity and local diagnostics into the output.
|
||||
// ResolveAs only ever returns user or bot, so the default branch handles user.
|
||||
func buildResult(cfg *core.CliConfig, as core.Identity, source string, diag identitydiag.Result) *whoamiResult {
|
||||
defaultAs := cfg.DefaultAs
|
||||
if defaultAs == "" {
|
||||
defaultAs = core.AsAuto
|
||||
}
|
||||
res := &whoamiResult{
|
||||
Profile: cfg.ProfileName,
|
||||
AppID: cfg.AppID,
|
||||
Brand: cfg.Brand,
|
||||
DefaultAs: string(defaultAs),
|
||||
Identity: string(as),
|
||||
IdentitySource: source,
|
||||
}
|
||||
// Use the diagnosed hint as-is: it is tailored to the credential source, so
|
||||
// it never says "auth login" when that is blocked under an external provider.
|
||||
switch as {
|
||||
case core.AsBot:
|
||||
res.Available = diag.Bot.Available
|
||||
res.TokenStatus = diag.Bot.Status
|
||||
if !diag.Bot.Available {
|
||||
res.Hint = diag.Bot.Hint
|
||||
}
|
||||
default: // user
|
||||
res.Available = diag.User.Available
|
||||
// Use Status (not the raw TokenStatus) so the vocab matches the bot
|
||||
// branch: "ready" means usable for both. available stays the canonical
|
||||
// usable signal; tokenStatus is the readable state behind it.
|
||||
res.TokenStatus = diag.User.Status
|
||||
// Set onBehalfOf only when a user is actually resolved; an unresolved
|
||||
// user identity (not signed in) has no one to act on behalf of yet.
|
||||
if diag.User.UserName != "" || diag.User.OpenID != "" {
|
||||
res.OnBehalfOf = &delegatedUser{UserName: diag.User.UserName, OpenID: diag.User.OpenID}
|
||||
}
|
||||
if !diag.User.Available {
|
||||
res.Hint = diag.User.Hint
|
||||
}
|
||||
}
|
||||
return res
|
||||
}
|
||||
@@ -1,320 +0,0 @@
|
||||
// Copyright (c) 2026 Lark Technologies Pte. Ltd.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package whoami
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/larksuite/cli/errs"
|
||||
extcred "github.com/larksuite/cli/extension/credential"
|
||||
"github.com/larksuite/cli/internal/cmdutil"
|
||||
"github.com/larksuite/cli/internal/core"
|
||||
"github.com/larksuite/cli/internal/credential"
|
||||
"github.com/larksuite/cli/internal/identitydiag"
|
||||
)
|
||||
|
||||
func TestResolveSource(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
changedAs bool
|
||||
flagAs core.Identity
|
||||
autoDetected bool
|
||||
strictForced core.Identity
|
||||
want string
|
||||
}{
|
||||
{"explicit flag user", true, core.AsUser, false, "", "flag"},
|
||||
{"explicit flag bot", true, core.AsBot, false, "", "flag"},
|
||||
{"flag auto falls through to auto-detect", true, core.AsAuto, true, "", "auto_detect"},
|
||||
{"auto detected", false, "", true, "", "auto_detect"},
|
||||
{"strict mode", false, "", false, core.AsBot, "strict_mode"},
|
||||
{"default_as", false, "", false, "", "default_as"},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got := resolveSource(tt.changedAs, tt.flagAs, tt.autoDetected, tt.strictForced)
|
||||
if got != tt.want {
|
||||
t.Errorf("resolveSource() = %q, want %q", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildResult_UserValid(t *testing.T) {
|
||||
cfg := &core.CliConfig{ProfileName: "my-app", AppID: "cli_x", Brand: core.BrandLark, DefaultAs: core.AsAuto}
|
||||
diag := identitydiag.Result{
|
||||
User: identitydiag.Identity{Available: true, Status: "ready", TokenStatus: "valid", OpenID: "ou_x", UserName: "Alice"},
|
||||
}
|
||||
r := buildResult(cfg, core.AsUser, "auto_detect", diag)
|
||||
|
||||
if r.Identity != "user" || r.IdentitySource != "auto_detect" {
|
||||
t.Fatalf("identity/source = %q/%q", r.Identity, r.IdentitySource)
|
||||
}
|
||||
// tokenStatus mirrors the unified Status vocab ("ready"), not the raw "valid".
|
||||
if !r.Available || r.TokenStatus != "ready" {
|
||||
t.Fatalf("available=%v status=%q", r.Available, r.TokenStatus)
|
||||
}
|
||||
if r.OnBehalfOf == nil || r.OnBehalfOf.OpenID != "ou_x" || r.OnBehalfOf.UserName != "Alice" {
|
||||
t.Fatalf("onBehalfOf = %#v, want Alice/ou_x", r.OnBehalfOf)
|
||||
}
|
||||
if r.Hint != "" {
|
||||
t.Fatalf("hint = %q, want empty", r.Hint)
|
||||
}
|
||||
if r.Profile != "my-app" || r.AppID != "cli_x" || r.Brand != core.BrandLark {
|
||||
t.Fatalf("app context = %#v", r)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildResult_UserMissingToken(t *testing.T) {
|
||||
cfg := &core.CliConfig{ProfileName: "p", AppID: "cli_x", Brand: core.BrandLark}
|
||||
diag := identitydiag.Result{
|
||||
User: identitydiag.Identity{Available: false, Status: "missing", Hint: "run: lark-cli auth login --help"}, // never logged in
|
||||
}
|
||||
r := buildResult(cfg, core.AsUser, "auto_detect", diag)
|
||||
|
||||
if r.Available {
|
||||
t.Fatalf("available = true, want false")
|
||||
}
|
||||
if r.TokenStatus != "missing" {
|
||||
t.Fatalf("tokenStatus = %q, want missing", r.TokenStatus)
|
||||
}
|
||||
// whoami renders the diagnosed hint verbatim (single source of truth) so it
|
||||
// stays correct for the external-provider path without whoami knowing about it.
|
||||
if r.Hint != diag.User.Hint {
|
||||
t.Fatalf("hint = %q, want propagated %q", r.Hint, diag.User.Hint)
|
||||
}
|
||||
if r.DefaultAs != "auto" {
|
||||
t.Fatalf("defaultAs = %q, want auto (empty normalized)", r.DefaultAs)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildResult_BotReady(t *testing.T) {
|
||||
cfg := &core.CliConfig{ProfileName: "p", AppID: "cli_x", Brand: core.BrandFeishu, DefaultAs: core.AsBot}
|
||||
diag := identitydiag.Result{
|
||||
Bot: identitydiag.Identity{Available: true, Status: "ready"},
|
||||
}
|
||||
r := buildResult(cfg, core.AsBot, "default_as", diag)
|
||||
|
||||
if r.Identity != "bot" || r.IdentitySource != "default_as" {
|
||||
t.Fatalf("identity/source = %q/%q", r.Identity, r.IdentitySource)
|
||||
}
|
||||
if !r.Available || r.TokenStatus != "ready" {
|
||||
t.Fatalf("available=%v status=%q", r.Available, r.TokenStatus)
|
||||
}
|
||||
if r.OnBehalfOf != nil {
|
||||
t.Fatalf("bot must not carry onBehalfOf: %#v", r.OnBehalfOf)
|
||||
}
|
||||
if r.Hint != "" {
|
||||
t.Fatalf("hint = %q, want empty", r.Hint)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildResult_BotNotConfigured(t *testing.T) {
|
||||
cfg := &core.CliConfig{ProfileName: "p", AppID: "cli_x", Brand: core.BrandFeishu}
|
||||
diag := identitydiag.Result{
|
||||
Bot: identitydiag.Identity{Available: false, Status: "not_configured", Hint: "run: lark-cli config --help"},
|
||||
}
|
||||
r := buildResult(cfg, core.AsBot, "auto_detect", diag)
|
||||
|
||||
if r.Available {
|
||||
t.Fatalf("available = true, want false")
|
||||
}
|
||||
if r.TokenStatus != "not_configured" {
|
||||
t.Fatalf("tokenStatus = %q, want not_configured", r.TokenStatus)
|
||||
}
|
||||
if r.Hint != diag.Bot.Hint {
|
||||
t.Fatalf("hint = %q, want propagated %q", r.Hint, diag.Bot.Hint)
|
||||
}
|
||||
}
|
||||
|
||||
func TestWhoami_BotJSON(t *testing.T) {
|
||||
f, stdout, _, _ := cmdutil.TestFactory(t, &core.CliConfig{
|
||||
ProfileName: "test-profile", AppID: "test-app", AppSecret: "test-secret", Brand: core.BrandFeishu,
|
||||
})
|
||||
|
||||
cmd := NewCmdWhoami(f)
|
||||
cmd.SetArgs([]string{}) // bare whoami: output is always JSON, no flag needed
|
||||
if err := cmd.Execute(); err != nil {
|
||||
t.Fatalf("Execute() error = %v", err)
|
||||
}
|
||||
|
||||
var got whoamiResult
|
||||
if err := json.Unmarshal(stdout.Bytes(), &got); err != nil {
|
||||
t.Fatalf("json.Unmarshal() error = %v\n%s", err, stdout.String())
|
||||
}
|
||||
if got.Identity != "bot" {
|
||||
t.Fatalf("identity = %q, want bot", got.Identity)
|
||||
}
|
||||
if !got.Available || got.TokenStatus != "ready" {
|
||||
t.Fatalf("available=%v status=%q, want true/ready", got.Available, got.TokenStatus)
|
||||
}
|
||||
if got.Profile != "test-profile" {
|
||||
t.Fatalf("profile = %q, want test-profile", got.Profile)
|
||||
}
|
||||
if got.IdentitySource == "" {
|
||||
t.Fatalf("identitySource empty")
|
||||
}
|
||||
if got.OnBehalfOf != nil {
|
||||
t.Fatalf("bot (self) must not carry onBehalfOf: %#v", got.OnBehalfOf)
|
||||
}
|
||||
}
|
||||
|
||||
func TestWhoami_RejectsInvalidAs(t *testing.T) {
|
||||
for _, bad := range []string{"admin", "USER", "bogus123", ""} {
|
||||
t.Run("as="+bad, func(t *testing.T) {
|
||||
f, _, _, _ := cmdutil.TestFactory(t, &core.CliConfig{
|
||||
ProfileName: "p", AppID: "test-app", AppSecret: "test-secret", Brand: core.BrandFeishu,
|
||||
})
|
||||
cmd := NewCmdWhoami(f)
|
||||
cmd.SetArgs([]string{"--as", bad})
|
||||
err := cmd.Execute()
|
||||
if err == nil {
|
||||
t.Fatalf("Execute() with --as %q = nil, want validation error", bad)
|
||||
}
|
||||
// Lock in the typed validation contract: an unsupported identity must
|
||||
// surface as a *errs.ValidationError on --as, not just any error.
|
||||
var ve *errs.ValidationError
|
||||
if !errors.As(err, &ve) {
|
||||
t.Fatalf("Execute() with --as %q: error type = %T, want *errs.ValidationError: %v", bad, err, err)
|
||||
}
|
||||
if ve.Subtype != errs.SubtypeInvalidArgument {
|
||||
t.Errorf("Subtype = %q, want %q", ve.Subtype, errs.SubtypeInvalidArgument)
|
||||
}
|
||||
if ve.Param != "--as" {
|
||||
t.Errorf("Param = %q, want %q", ve.Param, "--as")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestWhoami_ConfigErrorPropagates(t *testing.T) {
|
||||
f, _, _, _ := cmdutil.TestFactory(t, &core.CliConfig{
|
||||
ProfileName: "p", AppID: "test-app", AppSecret: "test-secret", Brand: core.BrandFeishu,
|
||||
})
|
||||
wantErr := fmt.Errorf("boom")
|
||||
f.Config = func() (*core.CliConfig, error) { return nil, wantErr }
|
||||
|
||||
cmd := NewCmdWhoami(f)
|
||||
cmd.SetArgs([]string{"--json"})
|
||||
err := cmd.Execute()
|
||||
if err == nil {
|
||||
t.Fatalf("Execute() error = nil, want propagated config error")
|
||||
}
|
||||
// The f.Config() failure must propagate unchanged, not be masked by a later
|
||||
// command-execution error.
|
||||
if !errors.Is(err, wantErr) {
|
||||
t.Fatalf("Execute() error = %v, want it to wrap %v", err, wantErr)
|
||||
}
|
||||
}
|
||||
|
||||
func TestWhoami_StrictModeRejectsCrossIdentity(t *testing.T) {
|
||||
// Bot-only account → strict mode bot. A real `--as user` call would be
|
||||
// rejected by CheckStrictMode; whoami must reject it identically rather than
|
||||
// previewing a user identity the next call would refuse.
|
||||
f, _, _, _ := cmdutil.TestFactory(t, &core.CliConfig{
|
||||
ProfileName: "p", AppID: "test-app", AppSecret: "test-secret", Brand: core.BrandFeishu,
|
||||
SupportedIdentities: 2, // bot only
|
||||
})
|
||||
cmd := NewCmdWhoami(f)
|
||||
cmd.SetArgs([]string{"--as", "user", "--json"})
|
||||
err := cmd.Execute()
|
||||
if err == nil {
|
||||
t.Fatalf("Execute() with --as user under strict bot = nil, want strict-mode rejection")
|
||||
}
|
||||
var ve *errs.ValidationError
|
||||
if !errors.As(err, &ve) {
|
||||
t.Fatalf("error type = %T, want *errs.ValidationError: %v", err, err)
|
||||
}
|
||||
}
|
||||
|
||||
type fakeExtProvider struct {
|
||||
name string
|
||||
account *extcred.Account
|
||||
}
|
||||
|
||||
func (p *fakeExtProvider) Name() string { return p.name }
|
||||
func (p *fakeExtProvider) ResolveAccount(context.Context) (*extcred.Account, error) {
|
||||
return p.account, nil
|
||||
}
|
||||
func (p *fakeExtProvider) ResolveToken(context.Context, extcred.TokenSpec) (*extcred.Token, error) {
|
||||
return nil, nil // no UAT served locally; whoami runs with verify=false
|
||||
}
|
||||
|
||||
func externalWhoamiFactory(cfg *core.CliConfig) (*cmdutil.Factory, *bytes.Buffer) {
|
||||
cred := credential.NewCredentialProvider(
|
||||
[]extcred.Provider{&fakeExtProvider{name: "corp-sso", account: &extcred.Account{AppID: cfg.AppID}}},
|
||||
nil, nil,
|
||||
func() (*http.Client, error) { return nil, nil },
|
||||
)
|
||||
out := &bytes.Buffer{}
|
||||
f := &cmdutil.Factory{
|
||||
Config: func() (*core.CliConfig, error) { return cfg, nil },
|
||||
Credential: cred,
|
||||
IOStreams: &cmdutil.IOStreams{Out: out, ErrOut: &bytes.Buffer{}},
|
||||
}
|
||||
return f, out
|
||||
}
|
||||
|
||||
// Regression for the external-provider blind spot: with credentials managed by
|
||||
// an extension provider, a signed-in user must read as available, and an
|
||||
// unavailable identity must not be told to "auth login" (which is blocked).
|
||||
func TestWhoami_ExternalProvider_UserReady(t *testing.T) {
|
||||
cfg := &core.CliConfig{
|
||||
ProfileName: "p", AppID: "cli_x", Brand: core.BrandFeishu,
|
||||
SupportedIdentities: uint8(extcred.SupportsAll), UserOpenId: "ou_x", UserName: "Alice",
|
||||
}
|
||||
f, out := externalWhoamiFactory(cfg)
|
||||
|
||||
cmd := NewCmdWhoami(f)
|
||||
cmd.SetArgs([]string{"--as", "user", "--json"})
|
||||
if err := cmd.Execute(); err != nil {
|
||||
t.Fatalf("Execute() error = %v", err)
|
||||
}
|
||||
var got whoamiResult
|
||||
if err := json.Unmarshal(out.Bytes(), &got); err != nil {
|
||||
t.Fatalf("Unmarshal: %v\n%s", err, out.String())
|
||||
}
|
||||
if got.Identity != "user" || !got.Available || got.TokenStatus != "ready" {
|
||||
t.Fatalf("got %#v, want user/available/ready", got)
|
||||
}
|
||||
if got.OnBehalfOf == nil || got.OnBehalfOf.UserName != "Alice" || got.OnBehalfOf.OpenID != "ou_x" {
|
||||
t.Fatalf("onBehalfOf = %#v, want Alice/ou_x (delegated)", got.OnBehalfOf)
|
||||
}
|
||||
if got.Hint != "" {
|
||||
t.Fatalf("hint = %q, want empty when available", got.Hint)
|
||||
}
|
||||
}
|
||||
|
||||
func TestWhoami_ExternalProvider_UserHintNotKeychain(t *testing.T) {
|
||||
cfg := &core.CliConfig{
|
||||
ProfileName: "p", AppID: "cli_x", Brand: core.BrandFeishu,
|
||||
SupportedIdentities: uint8(extcred.SupportsUser), // user supported but not signed in
|
||||
}
|
||||
f, out := externalWhoamiFactory(cfg)
|
||||
|
||||
cmd := NewCmdWhoami(f)
|
||||
cmd.SetArgs([]string{"--as", "user", "--json"})
|
||||
if err := cmd.Execute(); err != nil {
|
||||
t.Fatalf("Execute() error = %v", err)
|
||||
}
|
||||
var got whoamiResult
|
||||
if err := json.Unmarshal(out.Bytes(), &got); err != nil {
|
||||
t.Fatalf("Unmarshal: %v\n%s", err, out.String())
|
||||
}
|
||||
if got.Identity != "user" || got.Available {
|
||||
t.Fatalf("got identity=%q available=%v, want user/false", got.Identity, got.Available)
|
||||
}
|
||||
if strings.Contains(got.Hint, "auth login") {
|
||||
t.Fatalf("hint must not point at auth login under external provider: %q", got.Hint)
|
||||
}
|
||||
if !strings.Contains(got.Hint, "external") {
|
||||
t.Fatalf("hint should explain external management: %q", got.Hint)
|
||||
}
|
||||
}
|
||||
@@ -1,41 +0,0 @@
|
||||
// Copyright (c) 2026 Lark Technologies Pte. Ltd.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"embed"
|
||||
"fmt"
|
||||
"io/fs"
|
||||
"os"
|
||||
|
||||
"github.com/larksuite/cli/cmd"
|
||||
"github.com/larksuite/cli/internal/affordance"
|
||||
)
|
||||
|
||||
// embeddedContentFS bundles the agent-readable content that must ship in lockstep
|
||||
// with the binary: each skill's docs (SKILL.md + references/, plus whiteboard's
|
||||
// routes/ and scenes/) and the per-domain affordance guidance (affordance/*.md).
|
||||
// Machine-resource skill dirs (assets/, scripts/) are excluded. It's a whitelist —
|
||||
// a new content type is omitted until added to the embed list. The embed must live
|
||||
// in this root package because go:embed cannot reach up out of a package's dir.
|
||||
//
|
||||
//go:embed skills/*/SKILL.md skills/*/references skills/*/routes skills/*/scenes affordance/*.md
|
||||
var embeddedContentFS embed.FS
|
||||
|
||||
// init wires the embedded content into the CLI. It compiles into `go build .` but
|
||||
// not the single-file preview build (`go build ./main.go`), so that build stays
|
||||
// self-contained (shipping no embedded content). Assembly failures warn on stderr
|
||||
// rather than panicking — embedded content is nice-to-have, not load-bearing.
|
||||
func init() {
|
||||
if sub, err := fs.Sub(embeddedContentFS, "skills"); err != nil {
|
||||
fmt.Fprintln(os.Stderr, "warning: skills embed assembly failed, skills commands disabled:", err)
|
||||
} else {
|
||||
cmd.SetEmbeddedSkillContent(sub)
|
||||
}
|
||||
if sub, err := fs.Sub(embeddedContentFS, "affordance"); err != nil {
|
||||
fmt.Fprintln(os.Stderr, "warning: affordance embed assembly failed, command guidance disabled:", err)
|
||||
} else {
|
||||
affordance.SetSource(sub)
|
||||
}
|
||||
}
|
||||
@@ -1,259 +0,0 @@
|
||||
# `slides +create-svglide` Codex Runtime Design
|
||||
|
||||
Date: 2026-07-02
|
||||
Branch: `feat-svglide-07`
|
||||
Scope: first local-only version of `lark-cli slides +create-svglide`
|
||||
|
||||
## Result
|
||||
|
||||
Build `slides +create-svglide` as a staged local runtime for AnyGen SVG Slides. The command creates and manages a run directory that Codex can fill with generated content, assets, and SVG slides. The CLI owns state, prompts, schemas, validation, preview, receipts, and recovery. Codex owns LLM reasoning, web research, image/search execution, chart design, and SVG authoring.
|
||||
|
||||
The first version does not publish to Feishu Slides. It must produce a local, inspectable SVG deck workbench.
|
||||
|
||||
## Context
|
||||
|
||||
`feat-svglide-07` currently starts from the latest `origin/main` and has only the existing Slides XML shortcut surface. There is no current `+create-svglide` implementation on this branch.
|
||||
|
||||
The AnyGen SVG Slides prompt should be reused as contracts and workflow rules, not pasted as one large prompt. Its value is split across request interpretation, research, design brief, outline, `slide_content.md`, asset planning, SVG authoring, protocol validation, preview, and repair.
|
||||
|
||||
## Goals
|
||||
|
||||
- Add a staged `slides +create-svglide` command group.
|
||||
- Create a local run directory under a user-specified `--out` path, usually `.lark-slides/svglide-runs/<run-id>`.
|
||||
- Generate prompt task files that tell Codex exactly what to produce for each stage.
|
||||
- Generate JSON schemas for stage outputs.
|
||||
- Track stage state in `run.json`.
|
||||
- Validate JSON outputs, SVG protocol basics, asset href existence, slide count, placeholder slides, and preview generation.
|
||||
- Generate `preview.html` for local inspection.
|
||||
- Write receipts and `repair_queue.md` so failed runs can resume from the current stage.
|
||||
|
||||
## Non-Goals
|
||||
|
||||
- No online Feishu Slides creation.
|
||||
- No `slide_engine` or `slide` server changes.
|
||||
- No SVG-to-SXSD conversion.
|
||||
- No built-in model API provider.
|
||||
- No built-in web search, image generation, or image search client.
|
||||
- No complete 12-agent process runner.
|
||||
- No PPTX import/edit workflow.
|
||||
|
||||
## Command Surface
|
||||
|
||||
```bash
|
||||
lark-cli slides +create-svglide init --title "Demo" --input ./source.md --audience "..." --delivery-mode self_read --pages 8 --out ./.lark-slides/svglide-runs/demo
|
||||
lark-cli slides +create-svglide next <run-dir>
|
||||
lark-cli slides +create-svglide status <run-dir>
|
||||
lark-cli slides +create-svglide validate <run-dir>
|
||||
lark-cli slides +create-svglide preview <run-dir>
|
||||
```
|
||||
|
||||
`init` creates the run directory, writes the initial request files, schemas, stage prompts, and `run.json`.
|
||||
|
||||
`next` reads `run.json`, finds the next stage, verifies required inputs, renders or refreshes that stage's Codex task prompt, and reports the exact files Codex must create. It must not pretend LLM work is complete.
|
||||
|
||||
`status` checks declared outputs and receipts for each stage, then prints the current stage, missing files, and next useful command.
|
||||
|
||||
`validate` runs deterministic checks and writes validation receipts.
|
||||
|
||||
`preview` writes `preview.html` from `outline/deck.json` and `slides/*.svg`.
|
||||
|
||||
## Run Directory Contract
|
||||
|
||||
```text
|
||||
<run-dir>/
|
||||
run.json
|
||||
README.md
|
||||
request/request.json
|
||||
request/source_manifest.json
|
||||
research/research_notes.md
|
||||
research/sources.json
|
||||
brief/design_brief.json
|
||||
brief/visual_system.json
|
||||
outline/deck.json
|
||||
content/slide_content.md
|
||||
content/slide_content.json
|
||||
assets/assets_plan.json
|
||||
assets/images/
|
||||
assets/charts/
|
||||
slides/*.svg
|
||||
prompts/*.task.md
|
||||
schemas/*.schema.json
|
||||
receipts/*.json
|
||||
receipts/generation_summary.md
|
||||
repair_queue.md
|
||||
preview.html
|
||||
```
|
||||
|
||||
The run directory is local agent state. It should not be committed by default.
|
||||
|
||||
## State Model
|
||||
|
||||
`run.json` stores:
|
||||
|
||||
- version
|
||||
- runtime, always `codex` in v1
|
||||
- command name
|
||||
- title
|
||||
- created and updated timestamps
|
||||
- current stage
|
||||
- stage list with status, inputs, outputs, and receipt path
|
||||
- important artifact paths
|
||||
- policy flags: `publish_enabled=false`, `network_by_codex=true`, `image_generation_by_codex=true`, `overwrite=false`
|
||||
|
||||
Stage statuses:
|
||||
|
||||
```text
|
||||
pending
|
||||
ready
|
||||
in_progress
|
||||
done
|
||||
failed
|
||||
blocked
|
||||
needs_repair
|
||||
```
|
||||
|
||||
## Stage Design
|
||||
|
||||
### 1. request
|
||||
|
||||
Role: Request Interpreter
|
||||
|
||||
Input: CLI flags and local source path.
|
||||
|
||||
Output: `request/request.json`, `request/source_manifest.json`.
|
||||
|
||||
Validation: title, audience, delivery mode, page count, and source references must be explicit or marked missing.
|
||||
|
||||
### 2. research
|
||||
|
||||
Role: Researcher
|
||||
|
||||
Input: request files and source files.
|
||||
|
||||
Output: `research/research_notes.md`, `research/sources.json`.
|
||||
|
||||
Validation: key facts need source references. Codex may perform web research, but the CLI only validates resulting files.
|
||||
|
||||
### 3. design_brief
|
||||
|
||||
Role: Design Brief Resolver and Visual System Planner
|
||||
|
||||
Input: request and research outputs.
|
||||
|
||||
Output: `brief/design_brief.json`, `brief/visual_system.json`.
|
||||
|
||||
Validation: narrative spine, depth, tone, and visual system dimensions must be present.
|
||||
|
||||
### 4. outline
|
||||
|
||||
Role: Outline Planner
|
||||
|
||||
Input: design brief.
|
||||
|
||||
Output: `outline/deck.json`.
|
||||
|
||||
Validation: page count matches request; each slide has id, title, summary, role, and key message.
|
||||
|
||||
### 5. slide_content
|
||||
|
||||
Role: Content Builder
|
||||
|
||||
Input: deck outline and research notes.
|
||||
|
||||
Output: `content/slide_content.md`, `content/slide_content.json`.
|
||||
|
||||
Validation: every slide has key material, content blocks, and source notes. This is content planning, not final layout.
|
||||
|
||||
### 6. assets
|
||||
|
||||
Role: Asset Planner and Chart Generator
|
||||
|
||||
Input: slide content and visual system.
|
||||
|
||||
Output: `assets/assets_plan.json`, optional `assets/images/*`, optional `assets/charts/*.svg`.
|
||||
|
||||
Validation: every planned asset has purpose plus either a local path or a fallback. Chart takeaway must be written before chart type.
|
||||
|
||||
### 7. svg_author
|
||||
|
||||
Role: SVG Author
|
||||
|
||||
Input: deck, slide content, visual system, and assets.
|
||||
|
||||
Output: `slides/*.svg`.
|
||||
|
||||
Validation: each slide must contain more than a background. Each slide needs a background, title, visible content or visual element, semantic id, and valid SVG root.
|
||||
|
||||
### 8. validate_preview_repair
|
||||
|
||||
Role: Protocol Validator, Preview Agent, and Repair Agent
|
||||
|
||||
Input: generated slides.
|
||||
|
||||
Output: `receipts/lint.json`, `receipts/preview.json`, `repair_queue.md`, `preview.html`.
|
||||
|
||||
Validation: SVG protocol lint, local href checks, slide count match, preview write success, and unresolved issues recorded in the repair queue.
|
||||
|
||||
## Code Layout
|
||||
|
||||
```text
|
||||
shortcuts/slides/
|
||||
slides_create_svglide.go
|
||||
slides_create_svglide_test.go
|
||||
|
||||
internal/svglide/
|
||||
run.go
|
||||
init.go
|
||||
stage.go
|
||||
prompt.go
|
||||
schema.go
|
||||
validate.go
|
||||
preview.go
|
||||
receipt.go
|
||||
```
|
||||
|
||||
The shortcut package should stay thin. State, prompt rendering, validation, and preview logic belong in `internal/svglide` so they can be tested without a Cobra/runtime-heavy command harness.
|
||||
|
||||
## Skill Documentation
|
||||
|
||||
Update `skills/lark-slides/SKILL.md` and add a focused reference file for the local SVG runtime. The skill should explain that `+create-svglide` is local-only in v1, requires Codex to fill stage outputs, and must not be described as an online publish path.
|
||||
|
||||
## Error Handling
|
||||
|
||||
- Missing required inputs block the stage and write a receipt.
|
||||
- Invalid JSON or schema mismatch marks the stage failed.
|
||||
- Invalid SVG marks `needs_repair` and writes `repair_queue.md`.
|
||||
- Existing output paths are not overwritten unless an explicit overwrite policy is enabled.
|
||||
- Partially completed stages remain inspectable; reruns resume from the current stage.
|
||||
|
||||
## Tests
|
||||
|
||||
Unit tests:
|
||||
|
||||
- `init` creates the expected directory tree and `run.json`.
|
||||
- `init` refuses to overwrite an existing run directory by default.
|
||||
- `status` identifies missing outputs.
|
||||
- `next` renders the correct stage prompt and does not mark Codex-only stages done.
|
||||
- `validate` catches invalid SVG, missing hrefs, placeholder slides, and slide count mismatch.
|
||||
- `preview` writes HTML that references generated SVG files.
|
||||
|
||||
Fixtures:
|
||||
|
||||
- `testdata/svglide_run_valid/`
|
||||
- `testdata/svglide_run_invalid/`
|
||||
|
||||
No live end-to-end test is required for v1 because this version does not call Feishu APIs.
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
- A user can initialize a run directory from local input.
|
||||
- Codex can follow generated task prompts stage by stage.
|
||||
- The CLI can report status and missing artifacts.
|
||||
- The CLI can validate a completed local SVG deck.
|
||||
- The CLI can generate local preview HTML.
|
||||
- Failed validation produces actionable repair output.
|
||||
- No online presentation is created.
|
||||
|
||||
## Further Judgment
|
||||
|
||||
This design deliberately optimizes for artifact contracts rather than agent-count symmetry. Once the local runtime is stable, individual stages can be split into fuller agents without changing the run directory contract.
|
||||
2426
docs/vendor/anygen-svg/source.full.md
vendored
2426
docs/vendor/anygen-svg/source.full.md
vendored
File diff suppressed because it is too large
Load Diff
8
docs/vendor/anygen-svg/source.meta.json
vendored
8
docs/vendor/anygen-svg/source.meta.json
vendored
@@ -1,8 +0,0 @@
|
||||
{
|
||||
"doc_url": "https://bytedance.larkoffice.com/docx/KnCLd7xr5ohWONxhKsncZ3Lxnvd",
|
||||
"local_full_snapshot": "/Users/bytedance/Documents/Codex/2026-07-01/https-bytedance-larkoffice-com-docx-kncld7xr5ohwonxhksncz3lxnvd/outputs/lark_doc_KnCLd7xr5ohWONxhKsncZ3Lxnvd/full.md",
|
||||
"local_handoff": "/Users/bytedance/Documents/Codex/2026-07-01/https-bytedance-larkoffice-com-docx-kncld7xr5ohwonxhksncz3lxnvd/outputs/anygen-slides-svg-prompt-handoff.md",
|
||||
"fetched_by": "local export",
|
||||
"fetched_for": "slides +create-svglide AnyGen SVG prompt runtime experiment",
|
||||
"experiment_mode": "experiment_unrestricted_assets"
|
||||
}
|
||||
20
docs/vendor/anygen-svg/source.outline.md
vendored
20
docs/vendor/anygen-svg/source.outline.md
vendored
@@ -1,20 +0,0 @@
|
||||
# AnyGen SVG Slides Local Outline
|
||||
|
||||
Source full snapshot: `docs/vendor/anygen-svg/source.full.md`
|
||||
Source handoff: `/Users/bytedance/Documents/Codex/2026-07-01/https-bytedance-larkoffice-com-docx-kncld7xr5ohwonxhksncz3lxnvd/outputs/anygen-slides-svg-prompt-handoff.md`
|
||||
Remote doc: `https://bytedance.larkoffice.com/docx/KnCLd7xr5ohWONxhKsncZ3Lxnvd`
|
||||
|
||||
Required sections to split:
|
||||
|
||||
- System prompt(编排 / mode_system_prompt_svg)
|
||||
- SVG reference(协议 schema + 设计规范 / svg_reference)
|
||||
- resolve_design_brief
|
||||
- slide_outline
|
||||
- activate_slides_edit
|
||||
- slides_edit
|
||||
- finish_slides_edit
|
||||
- slide_organize
|
||||
- compute_custom_shape_bbox
|
||||
- generate_svg_chart
|
||||
- slides_convert
|
||||
- slides_parse_template
|
||||
@@ -319,7 +319,7 @@ func TestPermissionError_FullChain(t *testing.T) {
|
||||
WithHint("run: lark-cli auth login --scope %q", "mail:user_mailbox.message:send").
|
||||
WithMissingScopes("mail:user_mailbox.message:send").
|
||||
WithIdentity("user").
|
||||
WithConsoleURL("https://open.feishu.cn/page/scope-apply?clientID=cli_xxx&scopes=mail:user_mailbox.message:send")
|
||||
WithConsoleURL("https://open.feishu.cn/app/cli_xxx/auth")
|
||||
|
||||
if got.Category != errs.CategoryAuthorization {
|
||||
t.Errorf("Category = %q, want %q", got.Category, errs.CategoryAuthorization)
|
||||
@@ -419,7 +419,7 @@ func TestBuilder_WireFormat(t *testing.T) {
|
||||
WithHint("run lark-cli auth login --scope calendar:event:create").
|
||||
WithMissingScopes("calendar:event:create").
|
||||
WithIdentity("user").
|
||||
WithConsoleURL("https://open.feishu.cn/page/scope-apply?clientID=cli_xxx&scopes=calendar:event:create")
|
||||
WithConsoleURL("https://open.feishu.cn/app/cli_xxx/auth")
|
||||
|
||||
buf, err := json.Marshal(e)
|
||||
if err != nil {
|
||||
@@ -439,7 +439,7 @@ func TestBuilder_WireFormat(t *testing.T) {
|
||||
"hint": "run lark-cli auth login --scope calendar:event:create",
|
||||
"log_id": "20260520-0a1b2c3d",
|
||||
"identity": "user",
|
||||
"console_url": "https://open.feishu.cn/page/scope-apply?clientID=cli_xxx&scopes=calendar:event:create",
|
||||
"console_url": "https://open.feishu.cn/app/cli_xxx/auth",
|
||||
"missing_scopes": []any{"calendar:event:create"},
|
||||
}
|
||||
for k, want := range wantFields {
|
||||
|
||||
@@ -1,132 +0,0 @@
|
||||
// Copyright (c) 2026 Lark Technologies Pte. Ltd.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package im
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"strings"
|
||||
|
||||
"github.com/larksuite/cli/internal/event"
|
||||
)
|
||||
|
||||
// CardActionTriggerOutput is the flattened shape for card.action.trigger.
|
||||
type CardActionTriggerOutput struct {
|
||||
Type string `json:"type" desc:"Event type; always card.action.trigger"`
|
||||
EventID string `json:"event_id,omitempty" desc:"Globally unique event ID"`
|
||||
Timestamp string `json:"timestamp,omitempty" desc:"Event delivery time (ms timestamp string)" kind:"timestamp_ms"`
|
||||
OperatorID string `json:"operator_id,omitempty" desc:"Operator open_id" kind:"open_id"`
|
||||
MessageID string `json:"message_id,omitempty" desc:"Message ID of the card" kind:"message_id"`
|
||||
ChatID string `json:"chat_id,omitempty" desc:"Chat ID" kind:"chat_id"`
|
||||
Host string `json:"host,omitempty" desc:"Host type: im_message / im_top_notice"`
|
||||
Token string `json:"token,omitempty" desc:"Token for delay card update (valid 30 min, max 2 updates)"`
|
||||
ActionTag string `json:"action_tag,omitempty" desc:"Triggered element type: button/select_static/input/checker/etc"`
|
||||
ActionValue string `json:"action_value,omitempty" desc:"Developer-defined action value as JSON string"`
|
||||
ActionName string `json:"action_name,omitempty" desc:"Element name attribute"`
|
||||
FormValue string `json:"form_value,omitempty" desc:"Form submission values as JSON string (only on form submit)"`
|
||||
InputValue string `json:"input_value,omitempty" desc:"Input field value (only for input elements)"`
|
||||
Option string `json:"option,omitempty" desc:"Selected option value (for single-select dropdown)"`
|
||||
Options string `json:"options,omitempty" desc:"Selected options, comma-separated (for multi-select)"`
|
||||
Checked bool `json:"checked" desc:"Checkbox state (for checkbox elements)"`
|
||||
Timezone string `json:"timezone,omitempty" desc:"User timezone for date/time picker interactions"`
|
||||
CardContent string `json:"card_content,omitempty" desc:"Original card JSON content (body.content) auto-fetched via message get API at consume time using message_id; empty if message_id absent or fetch fails"`
|
||||
}
|
||||
|
||||
func processCardAction(ctx context.Context, rt event.APIClient, raw *event.RawEvent, _ map[string]string) (json.RawMessage, error) {
|
||||
var envelope struct {
|
||||
Header struct {
|
||||
EventID string `json:"event_id"`
|
||||
EventType string `json:"event_type"`
|
||||
CreateTime string `json:"create_time"`
|
||||
} `json:"header"`
|
||||
Event struct {
|
||||
Operator struct {
|
||||
OpenID string `json:"open_id"`
|
||||
} `json:"operator"`
|
||||
Token string `json:"token"`
|
||||
Host string `json:"host"`
|
||||
Action struct {
|
||||
Tag string `json:"tag"`
|
||||
Value map[string]interface{} `json:"value"`
|
||||
Name string `json:"name"`
|
||||
FormValue map[string]interface{} `json:"form_value"`
|
||||
InputValue string `json:"input_value"`
|
||||
Option string `json:"option"`
|
||||
Options []string `json:"options"`
|
||||
Checked bool `json:"checked"`
|
||||
Timezone string `json:"timezone"`
|
||||
} `json:"action"`
|
||||
Context struct {
|
||||
OpenMessageID string `json:"open_message_id"`
|
||||
OpenChatID string `json:"open_chat_id"`
|
||||
} `json:"context"`
|
||||
} `json:"event"`
|
||||
}
|
||||
if err := json.Unmarshal(raw.Payload, &envelope); err != nil {
|
||||
return raw.Payload, nil //nolint:nilerr // passthrough on malformed payload
|
||||
}
|
||||
|
||||
actionValue := marshalToString(envelope.Event.Action.Value)
|
||||
formValue := marshalToString(envelope.Event.Action.FormValue)
|
||||
options := strings.Join(envelope.Event.Action.Options, ",")
|
||||
|
||||
out := &CardActionTriggerOutput{
|
||||
Type: envelope.Header.EventType,
|
||||
EventID: envelope.Header.EventID,
|
||||
Timestamp: envelope.Header.CreateTime,
|
||||
OperatorID: envelope.Event.Operator.OpenID,
|
||||
MessageID: envelope.Event.Context.OpenMessageID,
|
||||
ChatID: envelope.Event.Context.OpenChatID,
|
||||
Host: envelope.Event.Host,
|
||||
Token: envelope.Event.Token,
|
||||
ActionTag: envelope.Event.Action.Tag,
|
||||
ActionValue: actionValue,
|
||||
ActionName: envelope.Event.Action.Name,
|
||||
FormValue: formValue,
|
||||
InputValue: envelope.Event.Action.InputValue,
|
||||
Option: envelope.Event.Action.Option,
|
||||
Options: options,
|
||||
Checked: envelope.Event.Action.Checked,
|
||||
Timezone: envelope.Event.Action.Timezone,
|
||||
}
|
||||
|
||||
if out.MessageID != "" && rt != nil {
|
||||
out.CardContent = fetchCardUserDSL(ctx, rt, out.MessageID)
|
||||
}
|
||||
|
||||
return json.Marshal(out)
|
||||
}
|
||||
|
||||
// fetchCardUserDSL gets the card message content via message get API.
|
||||
// Returns empty string on any failure — never blocks event consumption.
|
||||
func fetchCardUserDSL(ctx context.Context, rt event.APIClient, messageID string) string {
|
||||
path := "/open-apis/im/v1/messages/" + messageID + "?card_msg_content_type=user_card_content"
|
||||
resp, err := rt.CallAPI(ctx, "GET", path, nil)
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
var result struct {
|
||||
Code int `json:"code"`
|
||||
Msg string `json:"msg"`
|
||||
Data struct {
|
||||
Items []struct {
|
||||
Body struct {
|
||||
Content string `json:"content"`
|
||||
} `json:"body"`
|
||||
} `json:"items"`
|
||||
} `json:"data"`
|
||||
}
|
||||
if json.Unmarshal(resp, &result) != nil || result.Code != 0 || len(result.Data.Items) == 0 {
|
||||
return ""
|
||||
}
|
||||
return result.Data.Items[0].Body.Content
|
||||
}
|
||||
|
||||
func marshalToString(m map[string]interface{}) string {
|
||||
if len(m) == 0 {
|
||||
return ""
|
||||
}
|
||||
b, _ := json.Marshal(m)
|
||||
return string(b)
|
||||
}
|
||||
@@ -1,432 +0,0 @@
|
||||
// Copyright (c) 2026 Lark Technologies Pte. Ltd.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package im
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/larksuite/cli/internal/event"
|
||||
)
|
||||
|
||||
func TestCardActionTriggerRegistered(t *testing.T) {
|
||||
def, ok := event.Lookup("card.action.trigger")
|
||||
if !ok {
|
||||
t.Fatal("card.action.trigger should be registered via Keys()")
|
||||
}
|
||||
if def.Schema.Custom == nil {
|
||||
t.Error("card.action.trigger must set Schema.Custom")
|
||||
}
|
||||
if def.Process == nil {
|
||||
t.Error("card.action.trigger must set Process")
|
||||
}
|
||||
if len(def.Scopes) == 0 {
|
||||
t.Error("Scopes must not be empty")
|
||||
}
|
||||
}
|
||||
|
||||
func TestProcessCardAction_Button(t *testing.T) {
|
||||
payload := `{
|
||||
"schema": "2.0",
|
||||
"header": {
|
||||
"event_id": "ev_btn_001",
|
||||
"event_type": "card.action.trigger",
|
||||
"create_time": "1776409469273"
|
||||
},
|
||||
"event": {
|
||||
"operator": {"open_id": "ou_operator"},
|
||||
"token": "c-token-btn",
|
||||
"host": "im_message",
|
||||
"action": {
|
||||
"tag": "button",
|
||||
"value": {"key": "approve"},
|
||||
"name": "approve_btn",
|
||||
"form_value": {},
|
||||
"options": [],
|
||||
"checked": false
|
||||
},
|
||||
"context": {
|
||||
"open_message_id": "om_msg_001",
|
||||
"open_chat_id": "oc_chat_001"
|
||||
}
|
||||
}
|
||||
}`
|
||||
out := runCardAction(t, payload, nil)
|
||||
|
||||
if out.Type != "card.action.trigger" {
|
||||
t.Errorf("Type = %q, want card.action.trigger", out.Type)
|
||||
}
|
||||
if out.EventID != "ev_btn_001" {
|
||||
t.Errorf("EventID = %q", out.EventID)
|
||||
}
|
||||
if out.OperatorID != "ou_operator" {
|
||||
t.Errorf("OperatorID = %q", out.OperatorID)
|
||||
}
|
||||
if out.ActionTag != "button" {
|
||||
t.Errorf("ActionTag = %q, want button", out.ActionTag)
|
||||
}
|
||||
if out.ActionValue != `{"key":"approve"}` {
|
||||
t.Errorf("ActionValue = %q", out.ActionValue)
|
||||
}
|
||||
if out.ActionName != "approve_btn" {
|
||||
t.Errorf("ActionName = %q", out.ActionName)
|
||||
}
|
||||
if out.Token != "c-token-btn" {
|
||||
t.Errorf("Token = %q", out.Token)
|
||||
}
|
||||
if out.MessageID != "om_msg_001" {
|
||||
t.Errorf("MessageID = %q", out.MessageID)
|
||||
}
|
||||
if out.ChatID != "oc_chat_001" {
|
||||
t.Errorf("ChatID = %q", out.ChatID)
|
||||
}
|
||||
if out.Host != "im_message" {
|
||||
t.Errorf("Host = %q", out.Host)
|
||||
}
|
||||
if out.Timestamp != "1776409469273" {
|
||||
t.Errorf("Timestamp = %q", out.Timestamp)
|
||||
}
|
||||
}
|
||||
|
||||
func TestProcessCardAction_FormSubmit(t *testing.T) {
|
||||
payload := `{
|
||||
"schema": "2.0",
|
||||
"header": {
|
||||
"event_id": "ev_form_001",
|
||||
"event_type": "card.action.trigger",
|
||||
"create_time": "1776409469274"
|
||||
},
|
||||
"event": {
|
||||
"operator": {"open_id": "ou_form_user"},
|
||||
"token": "c-token-form",
|
||||
"host": "im_message",
|
||||
"action": {
|
||||
"tag": "button",
|
||||
"value": {},
|
||||
"name": "submit_btn",
|
||||
"form_value": {"name": "test-user", "reason": "testing"},
|
||||
"options": [],
|
||||
"checked": false
|
||||
},
|
||||
"context": {
|
||||
"open_message_id": "om_form_001",
|
||||
"open_chat_id": "oc_chat_002"
|
||||
}
|
||||
}
|
||||
}`
|
||||
out := runCardAction(t, payload, nil)
|
||||
|
||||
if out.FormValue != `{"name":"test-user","reason":"testing"}` {
|
||||
t.Errorf("FormValue = %q", out.FormValue)
|
||||
}
|
||||
if out.ActionTag != "button" {
|
||||
t.Errorf("ActionTag = %q, want button", out.ActionTag)
|
||||
}
|
||||
}
|
||||
|
||||
func TestProcessCardAction_MultiSelect(t *testing.T) {
|
||||
payload := `{
|
||||
"schema": "2.0",
|
||||
"header": {
|
||||
"event_id": "ev_ms_001",
|
||||
"event_type": "card.action.trigger",
|
||||
"create_time": "1776409469275"
|
||||
},
|
||||
"event": {
|
||||
"operator": {"open_id": "ou_ms_user"},
|
||||
"token": "c-token-ms",
|
||||
"host": "im_message",
|
||||
"action": {
|
||||
"tag": "multi_select_static",
|
||||
"value": {},
|
||||
"name": "multi_select",
|
||||
"options": ["opt_1", "opt_3"],
|
||||
"checked": false
|
||||
},
|
||||
"context": {
|
||||
"open_message_id": "om_ms_001",
|
||||
"open_chat_id": "oc_chat_003"
|
||||
}
|
||||
}
|
||||
}`
|
||||
out := runCardAction(t, payload, nil)
|
||||
|
||||
if out.Options != "opt_1,opt_3" {
|
||||
t.Errorf("Options = %q, want opt_1,opt_3", out.Options)
|
||||
}
|
||||
if out.ActionTag != "multi_select_static" {
|
||||
t.Errorf("ActionTag = %q", out.ActionTag)
|
||||
}
|
||||
}
|
||||
|
||||
func TestProcessCardAction_Input(t *testing.T) {
|
||||
payload := `{
|
||||
"schema": "2.0",
|
||||
"header": {
|
||||
"event_id": "ev_input_001",
|
||||
"event_type": "card.action.trigger",
|
||||
"create_time": "1776409469276"
|
||||
},
|
||||
"event": {
|
||||
"operator": {"open_id": "ou_input_user"},
|
||||
"token": "c-token-input",
|
||||
"host": "im_message",
|
||||
"action": {
|
||||
"tag": "input",
|
||||
"value": {},
|
||||
"name": "text_input",
|
||||
"input_value": "hello world",
|
||||
"options": [],
|
||||
"checked": false
|
||||
},
|
||||
"context": {
|
||||
"open_message_id": "om_input_001",
|
||||
"open_chat_id": "oc_chat_004"
|
||||
}
|
||||
}
|
||||
}`
|
||||
out := runCardAction(t, payload, nil)
|
||||
|
||||
if out.InputValue != "hello world" {
|
||||
t.Errorf("InputValue = %q", out.InputValue)
|
||||
}
|
||||
if out.ActionTag != "input" {
|
||||
t.Errorf("ActionTag = %q", out.ActionTag)
|
||||
}
|
||||
}
|
||||
|
||||
func TestProcessCardAction_DatePicker(t *testing.T) {
|
||||
payload := `{
|
||||
"schema": "2.0",
|
||||
"header": {
|
||||
"event_id": "ev_date_001",
|
||||
"event_type": "card.action.trigger",
|
||||
"create_time": "1776409469277"
|
||||
},
|
||||
"event": {
|
||||
"operator": {"open_id": "ou_date_user"},
|
||||
"token": "c-token-date",
|
||||
"host": "im_message",
|
||||
"action": {
|
||||
"tag": "date_picker",
|
||||
"value": {},
|
||||
"name": "date_selector",
|
||||
"option": "2024-04-01 +0800",
|
||||
"timezone": "Asia/Shanghai",
|
||||
"options": [],
|
||||
"checked": false
|
||||
},
|
||||
"context": {
|
||||
"open_message_id": "om_date_001",
|
||||
"open_chat_id": "oc_chat_005"
|
||||
}
|
||||
}
|
||||
}`
|
||||
out := runCardAction(t, payload, nil)
|
||||
|
||||
if out.Option != "2024-04-01 +0800" {
|
||||
t.Errorf("Option = %q", out.Option)
|
||||
}
|
||||
if out.Timezone != "Asia/Shanghai" {
|
||||
t.Errorf("Timezone = %q", out.Timezone)
|
||||
}
|
||||
}
|
||||
|
||||
func TestProcessCardAction_MalformedPayload(t *testing.T) {
|
||||
raw := &event.RawEvent{
|
||||
EventID: "ev_bad",
|
||||
EventType: "card.action.trigger",
|
||||
Payload: json.RawMessage(`not json`),
|
||||
Timestamp: time.Now(),
|
||||
}
|
||||
got, err := processCardAction(context.Background(), nil, raw, nil)
|
||||
if err != nil {
|
||||
t.Fatalf("Process should swallow parse errors, got %v", err)
|
||||
}
|
||||
if string(got) != "not json" {
|
||||
t.Errorf("malformed fallback output = %q, want original bytes", string(got))
|
||||
}
|
||||
}
|
||||
|
||||
func TestProcessCardAction_MessageGetSuccess(t *testing.T) {
|
||||
payload := `{
|
||||
"schema": "2.0",
|
||||
"header": {
|
||||
"event_id": "ev_mg_ok",
|
||||
"event_type": "card.action.trigger",
|
||||
"create_time": "1776409469278"
|
||||
},
|
||||
"event": {
|
||||
"operator": {"open_id": "ou_mg_user"},
|
||||
"token": "c-token-mg",
|
||||
"host": "im_message",
|
||||
"action": {
|
||||
"tag": "button",
|
||||
"value": {"key": "click"},
|
||||
"name": "btn",
|
||||
"form_value": {},
|
||||
"options": [],
|
||||
"checked": false
|
||||
},
|
||||
"context": {
|
||||
"open_message_id": "om_mg_001",
|
||||
"open_chat_id": "oc_chat_mg"
|
||||
}
|
||||
}
|
||||
}`
|
||||
cardContent := `{"header":{"title":{"tag":"plain_text","content":"A card"}}}`
|
||||
mock := &mockAPIClient{resp: `{
|
||||
"code": 0,
|
||||
"msg": "success",
|
||||
"data": {
|
||||
"items": [{
|
||||
"body": {"content": "` + escapeJSON(cardContent) + `"}
|
||||
}]
|
||||
}
|
||||
}`}
|
||||
out := runCardAction(t, payload, mock)
|
||||
|
||||
if out.CardContent == "" {
|
||||
t.Error("CardContent should not be empty when message get succeeds")
|
||||
}
|
||||
}
|
||||
|
||||
func TestProcessCardAction_MessageGetErrorCode(t *testing.T) {
|
||||
payload := `{
|
||||
"schema": "2.0",
|
||||
"header": {
|
||||
"event_id": "ev_mg_ec",
|
||||
"event_type": "card.action.trigger",
|
||||
"create_time": "1776409469279"
|
||||
},
|
||||
"event": {
|
||||
"operator": {"open_id": "ou_mg_user2"},
|
||||
"token": "c-token-mg2",
|
||||
"host": "im_message",
|
||||
"action": {
|
||||
"tag": "button",
|
||||
"value": {},
|
||||
"name": "btn",
|
||||
"form_value": {},
|
||||
"options": [],
|
||||
"checked": false
|
||||
},
|
||||
"context": {
|
||||
"open_message_id": "om_mg_002",
|
||||
"open_chat_id": "oc_chat_mg2"
|
||||
}
|
||||
}
|
||||
}`
|
||||
mock := &mockAPIClient{resp: `{"code": 1, "msg": "error", "data": {"items": []}}`}
|
||||
out := runCardAction(t, payload, mock)
|
||||
|
||||
if out.CardContent != "" {
|
||||
t.Errorf("CardContent should be empty when code != 0, got %q", out.CardContent)
|
||||
}
|
||||
}
|
||||
|
||||
func TestProcessCardAction_MessageGetFailure(t *testing.T) {
|
||||
payload := `{
|
||||
"schema": "2.0",
|
||||
"header": {
|
||||
"event_id": "ev_mg_fail",
|
||||
"event_type": "card.action.trigger",
|
||||
"create_time": "1776409469280"
|
||||
},
|
||||
"event": {
|
||||
"operator": {"open_id": "ou_mg_user3"},
|
||||
"token": "c-token-mg3",
|
||||
"host": "im_message",
|
||||
"action": {
|
||||
"tag": "button",
|
||||
"value": {},
|
||||
"name": "btn",
|
||||
"form_value": {},
|
||||
"options": [],
|
||||
"checked": false
|
||||
},
|
||||
"context": {
|
||||
"open_message_id": "om_mg_003",
|
||||
"open_chat_id": "oc_chat_mg3"
|
||||
}
|
||||
}
|
||||
}`
|
||||
mock := &mockAPIClient{errResp: true}
|
||||
out := runCardAction(t, payload, mock)
|
||||
|
||||
if out.CardContent != "" {
|
||||
t.Errorf("CardContent should be empty when message get fails, got %q", out.CardContent)
|
||||
}
|
||||
}
|
||||
|
||||
func TestProcessCardAction_EmptyMessageID(t *testing.T) {
|
||||
payload := `{
|
||||
"schema": "2.0",
|
||||
"header": {
|
||||
"event_id": "ev_no_msg",
|
||||
"event_type": "card.action.trigger",
|
||||
"create_time": "1776409469281"
|
||||
},
|
||||
"event": {
|
||||
"operator": {"open_id": "ou_no_msg"},
|
||||
"token": "c-token-nm",
|
||||
"host": "im_message",
|
||||
"action": {
|
||||
"tag": "button",
|
||||
"value": {},
|
||||
"name": "btn",
|
||||
"form_value": {},
|
||||
"options": [],
|
||||
"checked": false
|
||||
},
|
||||
"context": {
|
||||
"open_message_id": "",
|
||||
"open_chat_id": "oc_chat_nm"
|
||||
}
|
||||
}
|
||||
}`
|
||||
out := runCardAction(t, payload, nil)
|
||||
|
||||
if out.CardContent != "" {
|
||||
t.Errorf("CardContent should be empty when message_id is absent, got %q", out.CardContent)
|
||||
}
|
||||
}
|
||||
|
||||
type mockAPIClient struct {
|
||||
resp string
|
||||
errResp bool
|
||||
}
|
||||
|
||||
func (m *mockAPIClient) CallAPI(_ context.Context, _, _ string, _ interface{}) (json.RawMessage, error) {
|
||||
if m.errResp {
|
||||
return nil, context.DeadlineExceeded
|
||||
}
|
||||
return json.RawMessage(m.resp), nil
|
||||
}
|
||||
|
||||
func runCardAction(t *testing.T, payload string, rt event.APIClient) CardActionTriggerOutput {
|
||||
t.Helper()
|
||||
raw := &event.RawEvent{
|
||||
EventID: "ev_test",
|
||||
EventType: "card.action.trigger",
|
||||
Payload: json.RawMessage(payload),
|
||||
Timestamp: time.Now(),
|
||||
}
|
||||
got, err := processCardAction(context.Background(), rt, raw, nil)
|
||||
if err != nil {
|
||||
t.Fatalf("Process error: %v", err)
|
||||
}
|
||||
var out CardActionTriggerOutput
|
||||
if err := json.Unmarshal(got, &out); err != nil {
|
||||
t.Fatalf("Process output is not valid CardActionTriggerOutput JSON: %v\nraw=%s", err, string(got))
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func escapeJSON(s string) string {
|
||||
b, _ := json.Marshal(s)
|
||||
return string(b[1 : len(b)-1])
|
||||
}
|
||||
@@ -27,21 +27,6 @@ func Keys() []event.KeyDefinition {
|
||||
AuthTypes: []string{"bot"},
|
||||
RequiredConsoleEvents: []string{"im.message.receive_v1"},
|
||||
},
|
||||
{
|
||||
Key: "card.action.trigger",
|
||||
DisplayName: "Card action",
|
||||
Description: "Triggered when a user interacts with an interactive card (button click, form submit, dropdown select, etc.). Output includes: token (valid 30 min, max 2 updates), action details (tag, value, name, form_value), and card_content (original card in userDSL text format, auto-fetched at consume time). To update the card: parse card_content to understand the current state, construct the new card JSON, then call `lark-cli api POST /open-apis/interactive/v1/card/update` with the token (see lark-im-card-action-reply.md).",
|
||||
EventType: "card.action.trigger",
|
||||
SubscriptionType: event.SubTypeCallback,
|
||||
Schema: event.SchemaDef{
|
||||
Custom: &event.SchemaSpec{Type: reflect.TypeOf(CardActionTriggerOutput{})},
|
||||
},
|
||||
Process: processCardAction,
|
||||
Scopes: []string{"im:message:readonly"},
|
||||
AuthTypes: []string{"bot"},
|
||||
SingleConsumer: true,
|
||||
RequiredConsoleEvents: []string{"card.action.trigger"},
|
||||
},
|
||||
}
|
||||
|
||||
for _, rk := range nativeIMKeys {
|
||||
|
||||
@@ -7,7 +7,6 @@ package events
|
||||
import (
|
||||
"github.com/larksuite/cli/events/im"
|
||||
"github.com/larksuite/cli/events/minutes"
|
||||
"github.com/larksuite/cli/events/task"
|
||||
"github.com/larksuite/cli/events/vc"
|
||||
"github.com/larksuite/cli/events/whiteboard"
|
||||
"github.com/larksuite/cli/internal/event"
|
||||
@@ -18,7 +17,6 @@ func init() {
|
||||
all := [][]event.KeyDefinition{
|
||||
im.Keys(),
|
||||
minutes.Keys(),
|
||||
task.Keys(),
|
||||
vc.Keys(),
|
||||
whiteboard.Keys(),
|
||||
}
|
||||
|
||||
@@ -1,23 +0,0 @@
|
||||
// Copyright (c) 2026 Lark Technologies Pte. Ltd.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package task
|
||||
|
||||
// TaskUpdateUserAccessV2Data is the Task v2 update event payload under the
|
||||
// standard Lark V2 event envelope.
|
||||
type TaskUpdateUserAccessV2Data struct {
|
||||
EventTypes []string `json:"event_types,omitempty" desc:"Task commit types included in this event" enum:"task_create,task_deleted,task_summary_update,task_desc_update,task_assignees_update,task_followers_update,task_reminders_update,task_start_due_update,task_completed_update"`
|
||||
TaskGUID string `json:"task_guid,omitempty" desc:"Task GUID that changed" kind:"task_guid"`
|
||||
}
|
||||
|
||||
var taskUpdateUserAccessCommitTypes = []string{
|
||||
"task_create",
|
||||
"task_deleted",
|
||||
"task_summary_update",
|
||||
"task_desc_update",
|
||||
"task_assignees_update",
|
||||
"task_followers_update",
|
||||
"task_reminders_update",
|
||||
"task_start_due_update",
|
||||
"task_completed_update",
|
||||
}
|
||||
@@ -1,32 +0,0 @@
|
||||
// Copyright (c) 2026 Lark Technologies Pte. Ltd.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package task
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/larksuite/cli/errs"
|
||||
"github.com/larksuite/cli/internal/event"
|
||||
)
|
||||
|
||||
const taskSubscriptionPath = "/open-apis/task/v2/task_v2/task_subscription?user_id_type=open_id"
|
||||
|
||||
func taskSubscriptionPreConsume(ctx context.Context, rt event.APIClient, _ map[string]string) (func() error, error) {
|
||||
if rt == nil {
|
||||
return nil, errs.NewInternalError(errs.SubtypeUnknown,
|
||||
"runtime API client is required for pre-consume subscription")
|
||||
}
|
||||
|
||||
if _, err := rt.CallAPI(ctx, "POST", taskSubscriptionPath, nil); err != nil {
|
||||
if _, ok := errs.ProblemOf(err); ok {
|
||||
return nil, err
|
||||
}
|
||||
return nil, errs.NewNetworkError(
|
||||
errs.SubtypeNetworkTransport,
|
||||
"failed to subscribe task event",
|
||||
).WithCause(err)
|
||||
}
|
||||
|
||||
return nil, nil
|
||||
}
|
||||
@@ -1,119 +0,0 @@
|
||||
// Copyright (c) 2026 Lark Technologies Pte. Ltd.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package task
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"testing"
|
||||
|
||||
"github.com/larksuite/cli/errs"
|
||||
)
|
||||
|
||||
type stubAPIClient struct {
|
||||
err error
|
||||
|
||||
method string
|
||||
path string
|
||||
body interface{}
|
||||
calls int
|
||||
}
|
||||
|
||||
func (s *stubAPIClient) CallAPI(_ context.Context, method, path string, body interface{}) (json.RawMessage, error) {
|
||||
s.method = method
|
||||
s.path = path
|
||||
s.body = body
|
||||
s.calls++
|
||||
if s.err != nil {
|
||||
return nil, s.err
|
||||
}
|
||||
return json.RawMessage(`{"code":0,"msg":"success","data":{}}`), nil
|
||||
}
|
||||
|
||||
func TestTaskSubscriptionPreConsumeCallsSubscribeAPI(t *testing.T) {
|
||||
rt := &stubAPIClient{}
|
||||
cleanup, err := taskSubscriptionPreConsume(context.Background(), rt, nil)
|
||||
if err != nil {
|
||||
t.Fatalf("taskSubscriptionPreConsume error = %v", err)
|
||||
}
|
||||
if cleanup != nil {
|
||||
t.Fatal("cleanup = non-nil, want nil because task subscription has no unsubscribe API")
|
||||
}
|
||||
if rt.calls != 1 {
|
||||
t.Fatalf("calls = %d, want 1", rt.calls)
|
||||
}
|
||||
if rt.method != "POST" {
|
||||
t.Errorf("method = %q, want POST", rt.method)
|
||||
}
|
||||
if rt.path != taskSubscriptionPath {
|
||||
t.Errorf("path = %q, want %q", rt.path, taskSubscriptionPath)
|
||||
}
|
||||
if rt.body != nil {
|
||||
t.Errorf("body = %#v, want nil", rt.body)
|
||||
}
|
||||
}
|
||||
|
||||
func TestTaskSubscriptionPreConsumeRequiresRuntime(t *testing.T) {
|
||||
_, err := taskSubscriptionPreConsume(context.Background(), nil, nil)
|
||||
if err == nil {
|
||||
t.Fatal("expected error")
|
||||
}
|
||||
p, ok := errs.ProblemOf(err)
|
||||
if !ok {
|
||||
t.Fatalf("expected typed error, got %T: %v", err, err)
|
||||
}
|
||||
if p.Category != errs.CategoryInternal {
|
||||
t.Errorf("category = %s, want %s", p.Category, errs.CategoryInternal)
|
||||
}
|
||||
if p.Subtype != errs.SubtypeUnknown {
|
||||
t.Errorf("subtype = %s, want %s", p.Subtype, errs.SubtypeUnknown)
|
||||
}
|
||||
}
|
||||
|
||||
func TestTaskSubscriptionPreConsumePassesThroughAPIError(t *testing.T) {
|
||||
wantErr := errs.NewValidationError(errs.SubtypeFailedPrecondition, "subscription already exists")
|
||||
rt := &stubAPIClient{err: wantErr}
|
||||
|
||||
_, err := taskSubscriptionPreConsume(context.Background(), rt, nil)
|
||||
if err != wantErr {
|
||||
t.Fatalf("err identity changed: got %T %v, want original %T %v", err, err, wantErr, wantErr)
|
||||
}
|
||||
if !errors.Is(err, wantErr) {
|
||||
t.Fatalf("err = %v, want %v", err, wantErr)
|
||||
}
|
||||
p, ok := errs.ProblemOf(err)
|
||||
if !ok {
|
||||
t.Fatalf("expected typed error, got %T: %v", err, err)
|
||||
}
|
||||
if p.Category != errs.CategoryValidation {
|
||||
t.Errorf("category = %s, want %s", p.Category, errs.CategoryValidation)
|
||||
}
|
||||
if p.Subtype != errs.SubtypeFailedPrecondition {
|
||||
t.Errorf("subtype = %s, want %s", p.Subtype, errs.SubtypeFailedPrecondition)
|
||||
}
|
||||
}
|
||||
|
||||
func TestTaskSubscriptionPreConsumeWrapsUntypedAPIError(t *testing.T) {
|
||||
cause := errors.New("connection reset")
|
||||
rt := &stubAPIClient{err: cause}
|
||||
|
||||
_, err := taskSubscriptionPreConsume(context.Background(), rt, nil)
|
||||
if err == nil {
|
||||
t.Fatal("expected error")
|
||||
}
|
||||
if !errors.Is(err, cause) {
|
||||
t.Fatalf("err = %v, want cause %v", err, cause)
|
||||
}
|
||||
p, ok := errs.ProblemOf(err)
|
||||
if !ok {
|
||||
t.Fatalf("expected typed error, got %T: %v", err, err)
|
||||
}
|
||||
if p.Category != errs.CategoryNetwork {
|
||||
t.Errorf("category = %s, want %s", p.Category, errs.CategoryNetwork)
|
||||
}
|
||||
if p.Subtype != errs.SubtypeNetworkTransport {
|
||||
t.Errorf("subtype = %s, want %s", p.Subtype, errs.SubtypeNetworkTransport)
|
||||
}
|
||||
}
|
||||
@@ -1,33 +0,0 @@
|
||||
// Copyright (c) 2026 Lark Technologies Pte. Ltd.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
// Package task registers Task-domain EventKeys.
|
||||
package task
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
|
||||
"github.com/larksuite/cli/internal/event"
|
||||
)
|
||||
|
||||
const eventTypeTaskUpdateUserAccessV2 = "task.task.update_user_access_v2"
|
||||
|
||||
// Keys returns all Task-domain EventKey definitions.
|
||||
func Keys() []event.KeyDefinition {
|
||||
return []event.KeyDefinition{
|
||||
{
|
||||
Key: eventTypeTaskUpdateUserAccessV2,
|
||||
DisplayName: "Task updated",
|
||||
Description: "Triggered when tasks visible to the current user or app are created, deleted, or updated",
|
||||
EventType: eventTypeTaskUpdateUserAccessV2,
|
||||
Schema: event.SchemaDef{
|
||||
Native: &event.SchemaSpec{Type: reflect.TypeOf(TaskUpdateUserAccessV2Data{})},
|
||||
},
|
||||
PreConsume: taskSubscriptionPreConsume,
|
||||
Scopes: []string{"task:task:read"},
|
||||
AuthTypes: []string{"user", "bot"},
|
||||
RequiredConsoleEvents: []string{eventTypeTaskUpdateUserAccessV2},
|
||||
SingleConsumer: true,
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -1,95 +0,0 @@
|
||||
// Copyright (c) 2026 Lark Technologies Pte. Ltd.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package task
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"github.com/larksuite/cli/internal/event"
|
||||
"github.com/larksuite/cli/internal/event/schemas"
|
||||
)
|
||||
|
||||
func TestKeysTaskUpdateUserAccessMetadata(t *testing.T) {
|
||||
keys := Keys()
|
||||
if len(keys) != 1 {
|
||||
t.Fatalf("len(Keys()) = %d, want 1", len(keys))
|
||||
}
|
||||
|
||||
def := keys[0]
|
||||
if def.Key != eventTypeTaskUpdateUserAccessV2 {
|
||||
t.Errorf("Key = %q, want %q", def.Key, eventTypeTaskUpdateUserAccessV2)
|
||||
}
|
||||
if def.EventType != eventTypeTaskUpdateUserAccessV2 {
|
||||
t.Errorf("EventType = %q, want %q", def.EventType, eventTypeTaskUpdateUserAccessV2)
|
||||
}
|
||||
if def.Schema.Native == nil {
|
||||
t.Fatal("Schema.Native is nil")
|
||||
}
|
||||
if def.Schema.Native.Type != reflect.TypeOf(TaskUpdateUserAccessV2Data{}) {
|
||||
t.Errorf("native type = %v, want TaskUpdateUserAccessV2Data", def.Schema.Native.Type)
|
||||
}
|
||||
if def.Process != nil {
|
||||
t.Fatal("Native Task EventKey must not set Process")
|
||||
}
|
||||
if def.PreConsume == nil {
|
||||
t.Fatal("PreConsume is nil")
|
||||
}
|
||||
if !def.SingleConsumer {
|
||||
t.Fatal("SingleConsumer = false, want true")
|
||||
}
|
||||
if !reflect.DeepEqual(def.Scopes, []string{"task:task:read"}) {
|
||||
t.Errorf("Scopes = %#v", def.Scopes)
|
||||
}
|
||||
if !reflect.DeepEqual(def.AuthTypes, []string{"user", "bot"}) {
|
||||
t.Errorf("AuthTypes = %#v", def.AuthTypes)
|
||||
}
|
||||
if !reflect.DeepEqual(def.RequiredConsoleEvents, []string{eventTypeTaskUpdateUserAccessV2}) {
|
||||
t.Errorf("RequiredConsoleEvents = %#v", def.RequiredConsoleEvents)
|
||||
}
|
||||
}
|
||||
|
||||
func TestTaskUpdateUserAccessSchemaAnnotations(t *testing.T) {
|
||||
raw := schemas.WrapV2Envelope(schemas.FromType(reflect.TypeOf(TaskUpdateUserAccessV2Data{})))
|
||||
var schema map[string]interface{}
|
||||
if err := json.Unmarshal(raw, &schema); err != nil {
|
||||
t.Fatalf("unmarshal schema: %v", err)
|
||||
}
|
||||
|
||||
eventProps := schema["properties"].(map[string]interface{})["event"].(map[string]interface{})["properties"].(map[string]interface{})
|
||||
taskGUID := eventProps["task_guid"].(map[string]interface{})
|
||||
if got := taskGUID["format"]; got != "task_guid" {
|
||||
t.Errorf("task_guid format = %v, want task_guid", got)
|
||||
}
|
||||
|
||||
eventTypes := eventProps["event_types"].(map[string]interface{})
|
||||
items := eventTypes["items"].(map[string]interface{})
|
||||
rawEnum, ok := items["enum"].([]interface{})
|
||||
if !ok {
|
||||
t.Fatalf("event_types item enum missing: %#v", items["enum"])
|
||||
}
|
||||
got := make(map[string]bool, len(rawEnum))
|
||||
for _, v := range rawEnum {
|
||||
got[v.(string)] = true
|
||||
}
|
||||
for _, want := range taskUpdateUserAccessCommitTypes {
|
||||
if !got[want] {
|
||||
t.Errorf("event_types enum missing %q; enum=%v", want, rawEnum)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestTaskUpdateUserAccessRegistersCleanly(t *testing.T) {
|
||||
const key = eventTypeTaskUpdateUserAccessV2
|
||||
event.UnregisterKeyForTest(key)
|
||||
t.Cleanup(func() { event.UnregisterKeyForTest(key) })
|
||||
|
||||
for _, def := range Keys() {
|
||||
event.RegisterKey(def)
|
||||
}
|
||||
if _, ok := event.Lookup(key); !ok {
|
||||
t.Fatalf("event.Lookup(%q) not registered", key)
|
||||
}
|
||||
}
|
||||
@@ -1,62 +0,0 @@
|
||||
// Copyright (c) 2026 Lark Technologies Pte. Ltd.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package vc
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
|
||||
"github.com/larksuite/cli/internal/event"
|
||||
)
|
||||
|
||||
// VCParticipantMeetingJoinedOutput is the flattened shape for vc.meeting.participant_meeting_joined_v1.
|
||||
type VCParticipantMeetingJoinedOutput struct {
|
||||
Type string `json:"type" desc:"Event type; always vc.meeting.participant_meeting_joined_v1"`
|
||||
EventID string `json:"event_id,omitempty" desc:"Globally unique event ID; safe for deduplication"`
|
||||
Timestamp string `json:"timestamp,omitempty" desc:"Event delivery time (ms timestamp string); taken from header.create_time when present" kind:"timestamp_ms"`
|
||||
MeetingID string `json:"meeting_id,omitempty" desc:"Meeting ID" kind:"meeting_id"`
|
||||
Topic string `json:"topic,omitempty" desc:"Meeting topic"`
|
||||
MeetingNo string `json:"meeting_no,omitempty" desc:"Meeting number"`
|
||||
StartTime string `json:"start_time,omitempty" desc:"Meeting start time in RFC3339, converted to the local timezone"`
|
||||
CalendarEventID string `json:"calendar_event_id,omitempty" desc:"Calendar event ID associated with the meeting"`
|
||||
}
|
||||
|
||||
func processVCParticipantMeetingJoined(_ context.Context, _ event.APIClient, raw *event.RawEvent, _ map[string]string) (json.RawMessage, error) {
|
||||
var envelope struct {
|
||||
Header struct {
|
||||
EventID string `json:"event_id"`
|
||||
EventType string `json:"event_type"`
|
||||
CreateTime string `json:"create_time"`
|
||||
} `json:"header"`
|
||||
Event struct {
|
||||
Meeting struct {
|
||||
ID string `json:"id"`
|
||||
Topic string `json:"topic"`
|
||||
MeetingNo string `json:"meeting_no"`
|
||||
StartTime string `json:"start_time"`
|
||||
EndTime string `json:"end_time"`
|
||||
CalendarEventID string `json:"calendar_event_id"`
|
||||
} `json:"meeting"`
|
||||
} `json:"event"`
|
||||
}
|
||||
if err := json.Unmarshal(raw.Payload, &envelope); err != nil {
|
||||
return raw.Payload, nil //nolint:nilerr // passthrough on malformed payload so consumers still see the event
|
||||
}
|
||||
|
||||
meeting := envelope.Event.Meeting
|
||||
out := &VCParticipantMeetingJoinedOutput{
|
||||
Type: envelope.Header.EventType,
|
||||
EventID: envelope.Header.EventID,
|
||||
Timestamp: envelope.Header.CreateTime,
|
||||
MeetingID: meeting.ID,
|
||||
Topic: meeting.Topic,
|
||||
MeetingNo: meeting.MeetingNo,
|
||||
StartTime: unixSecondsToLocalRFC3339(meeting.StartTime),
|
||||
CalendarEventID: meeting.CalendarEventID,
|
||||
}
|
||||
if out.Type == "" {
|
||||
out.Type = raw.EventType
|
||||
}
|
||||
return json.Marshal(out)
|
||||
}
|
||||
@@ -1,281 +0,0 @@
|
||||
// Copyright (c) 2026 Lark Technologies Pte. Ltd.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package vc
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"reflect"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/larksuite/cli/internal/event"
|
||||
)
|
||||
|
||||
func TestVCKeys_ProcessedMeetingLifecycleRegistered(t *testing.T) {
|
||||
t.Setenv("LARKSUITE_CLI_CONFIG_DIR", t.TempDir())
|
||||
|
||||
for _, tc := range []struct {
|
||||
eventType string
|
||||
schemaType reflect.Type
|
||||
}{
|
||||
{eventTypeMeetingStarted, reflect.TypeOf(VCParticipantMeetingStartedOutput{})},
|
||||
{eventTypeMeetingJoined, reflect.TypeOf(VCParticipantMeetingJoinedOutput{})},
|
||||
} {
|
||||
t.Run(tc.eventType, func(t *testing.T) {
|
||||
def, ok := event.Lookup(tc.eventType)
|
||||
if !ok {
|
||||
t.Fatalf("%s should be registered via Keys()", tc.eventType)
|
||||
}
|
||||
if def.Schema.Custom == nil {
|
||||
t.Error("Processed key must set Schema.Custom")
|
||||
}
|
||||
if def.Schema.Native != nil {
|
||||
t.Error("Processed key must not set Schema.Native")
|
||||
}
|
||||
if def.Process == nil {
|
||||
t.Error("Process must not be nil for processed key")
|
||||
}
|
||||
if def.PreConsume == nil {
|
||||
t.Error("PreConsume must not be nil for processed key")
|
||||
}
|
||||
if len(def.Scopes) != 1 || def.Scopes[0] != "vc:meeting.meetingevent:read" {
|
||||
t.Errorf("Scopes = %v", def.Scopes)
|
||||
}
|
||||
if len(def.AuthTypes) != 1 || def.AuthTypes[0] != "user" {
|
||||
t.Errorf("AuthTypes = %v", def.AuthTypes)
|
||||
}
|
||||
if len(def.RequiredConsoleEvents) != 1 || def.RequiredConsoleEvents[0] != tc.eventType {
|
||||
t.Errorf("RequiredConsoleEvents = %v", def.RequiredConsoleEvents)
|
||||
}
|
||||
if def.Schema.Custom.Type != tc.schemaType {
|
||||
t.Errorf("Custom schema Type = %v, want %v", def.Schema.Custom.Type, tc.schemaType)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestProcessVCParticipantMeetingLifecycle(t *testing.T) {
|
||||
t.Setenv("LARKSUITE_CLI_CONFIG_DIR", t.TempDir())
|
||||
|
||||
for _, tc := range []struct {
|
||||
name string
|
||||
eventType string
|
||||
process event.ProcessFunc
|
||||
}{
|
||||
{
|
||||
name: "started",
|
||||
eventType: eventTypeMeetingStarted,
|
||||
process: processVCParticipantMeetingStarted,
|
||||
},
|
||||
{
|
||||
name: "joined",
|
||||
eventType: eventTypeMeetingJoined,
|
||||
process: processVCParticipantMeetingJoined,
|
||||
},
|
||||
} {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
payload := `{
|
||||
"schema": "2.0",
|
||||
"header": {
|
||||
"event_id": "ev_vc_lifecycle_001",
|
||||
"event_type": "` + tc.eventType + `",
|
||||
"create_time": "1608725989000",
|
||||
"app_id": "cli_test"
|
||||
},
|
||||
"event": {
|
||||
"meeting": {
|
||||
"id": "6911188411934433028",
|
||||
"topic": "my meeting",
|
||||
"meeting_no": "235812466",
|
||||
"start_time": "1608883322",
|
||||
"end_time": "1608883899",
|
||||
"calendar_event_id": "efa67a98-06a8-4df5-8559-746c8f4477ef_0"
|
||||
}
|
||||
}
|
||||
}`
|
||||
out := runMeetingLifecycleMap(t, tc.eventType, tc.process, payload)
|
||||
|
||||
if out["type"] != tc.eventType {
|
||||
t.Errorf("type = %q", out["type"])
|
||||
}
|
||||
if out["event_id"] != "ev_vc_lifecycle_001" {
|
||||
t.Errorf("event_id = %q", out["event_id"])
|
||||
}
|
||||
if out["timestamp"] != "1608725989000" {
|
||||
t.Errorf("timestamp = %q", out["timestamp"])
|
||||
}
|
||||
if out["meeting_id"] != "6911188411934433028" {
|
||||
t.Errorf("meeting_id = %q", out["meeting_id"])
|
||||
}
|
||||
if out["topic"] != "my meeting" || out["meeting_no"] != "235812466" {
|
||||
t.Errorf("topic/meeting_no = %q/%q", out["topic"], out["meeting_no"])
|
||||
}
|
||||
if out["calendar_event_id"] != "efa67a98-06a8-4df5-8559-746c8f4477ef_0" {
|
||||
t.Errorf("calendar_event_id = %q", out["calendar_event_id"])
|
||||
}
|
||||
if want := time.Unix(1608883322, 0).Local().Format(time.RFC3339); out["start_time"] != want {
|
||||
t.Errorf("start_time = %q, want %q", out["start_time"], want)
|
||||
}
|
||||
if _, hasEndTime := out["end_time"]; hasEndTime {
|
||||
t.Error("end_time should not be present in started/joined output")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestProcessVCParticipantMeetingLifecycle_InvalidMeetingTimes(t *testing.T) {
|
||||
t.Setenv("LARKSUITE_CLI_CONFIG_DIR", t.TempDir())
|
||||
|
||||
for _, tc := range []struct {
|
||||
name string
|
||||
eventType string
|
||||
process event.ProcessFunc
|
||||
}{
|
||||
{"started", eventTypeMeetingStarted, processVCParticipantMeetingStarted},
|
||||
{"joined", eventTypeMeetingJoined, processVCParticipantMeetingJoined},
|
||||
} {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
payload := `{
|
||||
"schema": "2.0",
|
||||
"header": {
|
||||
"event_id": "ev_vc_lifecycle_002",
|
||||
"event_type": "` + tc.eventType + `",
|
||||
"create_time": "1608725989001"
|
||||
},
|
||||
"event": {
|
||||
"meeting": {
|
||||
"id": "meeting_invalid_time",
|
||||
"start_time": "bad",
|
||||
"end_time": ""
|
||||
}
|
||||
}
|
||||
}`
|
||||
out := runMeetingLifecycleRaw(t, tc.eventType, tc.process, payload)
|
||||
switch tc.eventType {
|
||||
case eventTypeMeetingStarted:
|
||||
var started VCParticipantMeetingStartedOutput
|
||||
if err := json.Unmarshal(out, &started); err != nil {
|
||||
t.Fatalf("Process output is not valid started JSON: %v\nraw=%s", err, string(out))
|
||||
}
|
||||
if started.StartTime != "" {
|
||||
t.Errorf("StartTime = %q, want empty string", started.StartTime)
|
||||
}
|
||||
case eventTypeMeetingJoined:
|
||||
var joined VCParticipantMeetingJoinedOutput
|
||||
if err := json.Unmarshal(out, &joined); err != nil {
|
||||
t.Fatalf("Process output is not valid joined JSON: %v\nraw=%s", err, string(out))
|
||||
}
|
||||
if joined.StartTime != "" {
|
||||
t.Errorf("StartTime = %q, want empty string", joined.StartTime)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestProcessVCParticipantMeetingLifecycle_MalformedPayload(t *testing.T) {
|
||||
t.Setenv("LARKSUITE_CLI_CONFIG_DIR", t.TempDir())
|
||||
|
||||
for _, tc := range []struct {
|
||||
name string
|
||||
eventType string
|
||||
process event.ProcessFunc
|
||||
}{
|
||||
{"started", eventTypeMeetingStarted, processVCParticipantMeetingStarted},
|
||||
{"joined", eventTypeMeetingJoined, processVCParticipantMeetingJoined},
|
||||
} {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
raw := &event.RawEvent{
|
||||
EventType: tc.eventType,
|
||||
Payload: json.RawMessage(`not json`),
|
||||
Timestamp: time.Now(),
|
||||
}
|
||||
got, err := tc.process(context.Background(), nil, raw, nil)
|
||||
if err != nil {
|
||||
t.Fatalf("Process should swallow parse errors, got %v", err)
|
||||
}
|
||||
if string(got) != "not json" {
|
||||
t.Errorf("malformed fallback output = %q, want original bytes", string(got))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestVCParticipantMeetingLifecycle_PreConsumeSubscriptionLifecycle(t *testing.T) {
|
||||
t.Setenv("LARKSUITE_CLI_CONFIG_DIR", t.TempDir())
|
||||
|
||||
for _, eventType := range []string{eventTypeMeetingStarted, eventTypeMeetingJoined} {
|
||||
t.Run(eventType, func(t *testing.T) {
|
||||
def, ok := event.Lookup(eventType)
|
||||
if !ok {
|
||||
t.Fatalf("%s should be registered via Keys()", eventType)
|
||||
}
|
||||
|
||||
type call struct {
|
||||
method string
|
||||
path string
|
||||
body any
|
||||
}
|
||||
var calls []call
|
||||
rt := &stubAPIClient{
|
||||
callFn: func(_ context.Context, method, path string, body any) (json.RawMessage, error) {
|
||||
calls = append(calls, call{method: method, path: path, body: body})
|
||||
return json.RawMessage(`{"code":0,"msg":"success","data":{}}`), nil
|
||||
},
|
||||
}
|
||||
|
||||
cleanup, err := def.PreConsume(context.Background(), rt, nil)
|
||||
if err != nil {
|
||||
t.Fatalf("PreConsume error: %v", err)
|
||||
}
|
||||
if cleanup == nil {
|
||||
t.Fatal("cleanup must not be nil")
|
||||
}
|
||||
if len(calls) != 1 {
|
||||
t.Fatalf("calls after subscribe = %d, want 1", len(calls))
|
||||
}
|
||||
if calls[0].method != "POST" || calls[0].path != pathMeetingSubscribe {
|
||||
t.Fatalf("subscribe call = %+v", calls[0])
|
||||
}
|
||||
assertSubscriptionRequest(t, calls[0].body, eventType)
|
||||
|
||||
cleanup()
|
||||
if len(calls) != 2 {
|
||||
t.Fatalf("calls after cleanup = %d, want 2", len(calls))
|
||||
}
|
||||
if calls[1].method != "POST" || calls[1].path != pathMeetingUnsubscribe {
|
||||
t.Fatalf("unsubscribe call = %+v", calls[1])
|
||||
}
|
||||
assertSubscriptionRequest(t, calls[1].body, eventType)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func runMeetingLifecycleMap(t *testing.T, eventType string, process event.ProcessFunc, payload string) map[string]string {
|
||||
t.Helper()
|
||||
got := runMeetingLifecycleRaw(t, eventType, process, payload)
|
||||
if got == nil {
|
||||
t.Fatal("Process output is nil")
|
||||
}
|
||||
var out map[string]string
|
||||
if err := json.Unmarshal(got, &out); err != nil {
|
||||
t.Fatalf("Process output is not valid flat JSON object: %v\nraw=%s", err, string(got))
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func runMeetingLifecycleRaw(t *testing.T, eventType string, process event.ProcessFunc, payload string) json.RawMessage {
|
||||
t.Helper()
|
||||
raw := &event.RawEvent{
|
||||
EventType: eventType,
|
||||
Payload: json.RawMessage(payload),
|
||||
Timestamp: time.Now(),
|
||||
}
|
||||
got, err := process(context.Background(), nil, raw, nil)
|
||||
if err != nil {
|
||||
t.Fatalf("Process error: %v", err)
|
||||
}
|
||||
return got
|
||||
}
|
||||
@@ -1,61 +0,0 @@
|
||||
// Copyright (c) 2026 Lark Technologies Pte. Ltd.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package vc
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
|
||||
"github.com/larksuite/cli/internal/event"
|
||||
)
|
||||
|
||||
// VCParticipantMeetingStartedOutput is the flattened shape for vc.meeting.participant_meeting_started_v1.
|
||||
type VCParticipantMeetingStartedOutput struct {
|
||||
Type string `json:"type" desc:"Event type; always vc.meeting.participant_meeting_started_v1"`
|
||||
EventID string `json:"event_id,omitempty" desc:"Globally unique event ID; safe for deduplication"`
|
||||
Timestamp string `json:"timestamp,omitempty" desc:"Event delivery time (ms timestamp string); taken from header.create_time when present" kind:"timestamp_ms"`
|
||||
MeetingID string `json:"meeting_id,omitempty" desc:"Meeting ID" kind:"meeting_id"`
|
||||
Topic string `json:"topic,omitempty" desc:"Meeting topic"`
|
||||
MeetingNo string `json:"meeting_no,omitempty" desc:"Meeting number"`
|
||||
StartTime string `json:"start_time,omitempty" desc:"Meeting start time in RFC3339, converted to the local timezone"`
|
||||
CalendarEventID string `json:"calendar_event_id,omitempty" desc:"Calendar event ID associated with the meeting"`
|
||||
}
|
||||
|
||||
func processVCParticipantMeetingStarted(_ context.Context, _ event.APIClient, raw *event.RawEvent, _ map[string]string) (json.RawMessage, error) {
|
||||
var envelope struct {
|
||||
Header struct {
|
||||
EventID string `json:"event_id"`
|
||||
EventType string `json:"event_type"`
|
||||
CreateTime string `json:"create_time"`
|
||||
} `json:"header"`
|
||||
Event struct {
|
||||
Meeting struct {
|
||||
ID string `json:"id"`
|
||||
Topic string `json:"topic"`
|
||||
MeetingNo string `json:"meeting_no"`
|
||||
StartTime string `json:"start_time"`
|
||||
CalendarEventID string `json:"calendar_event_id"`
|
||||
} `json:"meeting"`
|
||||
} `json:"event"`
|
||||
}
|
||||
if err := json.Unmarshal(raw.Payload, &envelope); err != nil {
|
||||
return raw.Payload, nil //nolint:nilerr // passthrough on malformed payload so consumers still see the event
|
||||
}
|
||||
|
||||
meeting := envelope.Event.Meeting
|
||||
out := &VCParticipantMeetingStartedOutput{
|
||||
Type: envelope.Header.EventType,
|
||||
EventID: envelope.Header.EventID,
|
||||
Timestamp: envelope.Header.CreateTime,
|
||||
MeetingID: meeting.ID,
|
||||
Topic: meeting.Topic,
|
||||
MeetingNo: meeting.MeetingNo,
|
||||
StartTime: unixSecondsToLocalRFC3339(meeting.StartTime),
|
||||
CalendarEventID: meeting.CalendarEventID,
|
||||
}
|
||||
if out.Type == "" {
|
||||
out.Type = raw.EventType
|
||||
}
|
||||
return json.Marshal(out)
|
||||
}
|
||||
@@ -11,8 +11,6 @@ import (
|
||||
)
|
||||
|
||||
const (
|
||||
eventTypeMeetingStarted = "vc.meeting.participant_meeting_started_v1"
|
||||
eventTypeMeetingJoined = "vc.meeting.participant_meeting_joined_v1"
|
||||
eventTypeMeetingEnded = "vc.meeting.participant_meeting_ended_v1"
|
||||
eventTypeNoteGenerated = "vc.note.generated_v1"
|
||||
eventTypeRecordingStarted = "vc.recording.recording_started_v1"
|
||||
@@ -32,38 +30,6 @@ const (
|
||||
// Keys returns all VC-domain EventKey definitions.
|
||||
func Keys() []event.KeyDefinition {
|
||||
return []event.KeyDefinition{
|
||||
{
|
||||
Key: eventTypeMeetingStarted,
|
||||
DisplayName: "Participant meeting started",
|
||||
Description: "Triggered when a meeting the current user participates in has started",
|
||||
EventType: eventTypeMeetingStarted,
|
||||
Schema: event.SchemaDef{
|
||||
Custom: &event.SchemaSpec{Type: reflect.TypeOf(VCParticipantMeetingStartedOutput{})},
|
||||
},
|
||||
Process: processVCParticipantMeetingStarted,
|
||||
PreConsume: subscriptionPreConsume(eventTypeMeetingStarted, pathMeetingSubscribe, pathMeetingUnsubscribe),
|
||||
Scopes: []string{"vc:meeting.meetingevent:read"},
|
||||
AuthTypes: []string{
|
||||
"user",
|
||||
},
|
||||
RequiredConsoleEvents: []string{eventTypeMeetingStarted},
|
||||
},
|
||||
{
|
||||
Key: eventTypeMeetingJoined,
|
||||
DisplayName: "Participant meeting joined",
|
||||
Description: "Triggered when the current user joins a meeting",
|
||||
EventType: eventTypeMeetingJoined,
|
||||
Schema: event.SchemaDef{
|
||||
Custom: &event.SchemaSpec{Type: reflect.TypeOf(VCParticipantMeetingJoinedOutput{})},
|
||||
},
|
||||
Process: processVCParticipantMeetingJoined,
|
||||
PreConsume: subscriptionPreConsume(eventTypeMeetingJoined, pathMeetingSubscribe, pathMeetingUnsubscribe),
|
||||
Scopes: []string{"vc:meeting.meetingevent:read"},
|
||||
AuthTypes: []string{
|
||||
"user",
|
||||
},
|
||||
RequiredConsoleEvents: []string{eventTypeMeetingJoined},
|
||||
},
|
||||
{
|
||||
Key: eventTypeMeetingEnded,
|
||||
DisplayName: "Participant meeting ended",
|
||||
|
||||
5
harness-opt/.gitignore
vendored
Normal file
5
harness-opt/.gitignore
vendored
Normal file
@@ -0,0 +1,5 @@
|
||||
# harness-opt 只入库轻量决策记录;重的原始评测 run 不进版本库(dashboard 仍读磁盘)。
|
||||
baseline/runs/
|
||||
**/child-runs/
|
||||
verify_results/sealed-runs/
|
||||
verify_results/*-runs/
|
||||
5
harness-opt/baseline/baseline-tokens.json
Normal file
5
harness-opt/baseline/baseline-tokens.json
Normal file
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"1": 30086,
|
||||
"2": 34616,
|
||||
"3": 31289
|
||||
}
|
||||
50
harness-opt/baseline/noise-floor.json
Normal file
50
harness-opt/baseline/noise-floor.json
Normal file
@@ -0,0 +1,50 @@
|
||||
{
|
||||
"k": 5,
|
||||
"metrics": {
|
||||
"success_rate": {
|
||||
"mean": 0.4666666666666666,
|
||||
"std": 0.1632993161855452,
|
||||
"k": 5,
|
||||
"band": [
|
||||
0.14006803429557624,
|
||||
0.793265299037757
|
||||
]
|
||||
},
|
||||
"mean_score": {
|
||||
"mean": 0.5111111111111111,
|
||||
"std": 0.1507184440694504,
|
||||
"k": 5,
|
||||
"band": [
|
||||
0.20967422297221028,
|
||||
0.8125479992500119
|
||||
]
|
||||
},
|
||||
"mean_context_window": {
|
||||
"mean": 31997.0,
|
||||
"std": 7166.8411203573105,
|
||||
"k": 5,
|
||||
"band": [
|
||||
17663.31775928538,
|
||||
46330.682240714625
|
||||
]
|
||||
},
|
||||
"mean_duration_ms": {
|
||||
"mean": 50188.86666666667,
|
||||
"std": 7746.3168641619595,
|
||||
"k": 5,
|
||||
"band": [
|
||||
34696.23293834275,
|
||||
65681.50039499058
|
||||
]
|
||||
},
|
||||
"mean_token": {
|
||||
"mean": 263981.06666666665,
|
||||
"std": 27890.193480385413,
|
||||
"k": 5,
|
||||
"band": [
|
||||
208200.67970589583,
|
||||
319761.45362743747
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
33
harness-opt/baseline/summary.json
Normal file
33
harness-opt/baseline/summary.json
Normal file
@@ -0,0 +1,33 @@
|
||||
{
|
||||
"k": 5,
|
||||
"n_cases": 3,
|
||||
"effect": {
|
||||
"mean": 0.5111111111111111,
|
||||
"sigma": 0.1507184440694504
|
||||
},
|
||||
"token": {
|
||||
"mean": 31997.0,
|
||||
"sigma": 7166.8411203573105
|
||||
},
|
||||
"duration": {
|
||||
"mean": 50188.86666666667,
|
||||
"sigma": 7746.3168641619595
|
||||
},
|
||||
"phi0_per_case": {
|
||||
"1": {
|
||||
"effect": 0.6,
|
||||
"token": 30086,
|
||||
"duration": 51004
|
||||
},
|
||||
"2": {
|
||||
"effect": 0.4,
|
||||
"token": 34616,
|
||||
"duration": 52787
|
||||
},
|
||||
"3": {
|
||||
"effect": 0.5333,
|
||||
"token": 31289,
|
||||
"duration": 46776
|
||||
}
|
||||
}
|
||||
}
|
||||
869
harness-opt/coverage.json
Normal file
869
harness-opt/coverage.json
Normal file
@@ -0,0 +1,869 @@
|
||||
{
|
||||
"summary": {
|
||||
"total_cases": 3,
|
||||
"files": 25,
|
||||
"expected_declared": 0,
|
||||
"blind_spots": 22,
|
||||
"overfit_high": 5,
|
||||
"suggest_add_cases": [
|
||||
"skills/lark-im/references/lark-im-chat-identity.md",
|
||||
"skills/lark-im/references/lark-im-flag-cancel.md",
|
||||
"skills/lark-im/references/lark-im-flag-create.md",
|
||||
"skills/lark-im/references/lark-im-message-enrichment.md",
|
||||
"skills/lark-im/references/lark-im-messages-search.md"
|
||||
],
|
||||
"suggest_fix_routing": []
|
||||
},
|
||||
"files": [
|
||||
{
|
||||
"path": "skills/lark-im/references/lark-im-chat-identity.md",
|
||||
"is_domain_skill": false,
|
||||
"actual": {
|
||||
"count": 0,
|
||||
"pct": 0.0,
|
||||
"tier": "盲区"
|
||||
},
|
||||
"expected": {
|
||||
"count": 0,
|
||||
"pct": 0.0,
|
||||
"tier": "盲区"
|
||||
},
|
||||
"union": {
|
||||
"count": 0,
|
||||
"pct": 0.0,
|
||||
"tier": "盲区"
|
||||
},
|
||||
"discoverability_miss": 0,
|
||||
"density_count": 0,
|
||||
"density_pct": 0.0,
|
||||
"density_tier": "盲区",
|
||||
"risk_tier": "高",
|
||||
"risk_lines": {
|
||||
"R0": 5,
|
||||
"R1": 0,
|
||||
"R2": 0,
|
||||
"R3": 50
|
||||
},
|
||||
"total_lines": 55,
|
||||
"overfit_risk": "高",
|
||||
"suggest_add_cases": true,
|
||||
"suggest_fix_routing": false
|
||||
},
|
||||
{
|
||||
"path": "skills/lark-im/references/lark-im-messages-search.md",
|
||||
"is_domain_skill": false,
|
||||
"actual": {
|
||||
"count": 0,
|
||||
"pct": 0.0,
|
||||
"tier": "盲区"
|
||||
},
|
||||
"expected": {
|
||||
"count": 0,
|
||||
"pct": 0.0,
|
||||
"tier": "盲区"
|
||||
},
|
||||
"union": {
|
||||
"count": 0,
|
||||
"pct": 0.0,
|
||||
"tier": "盲区"
|
||||
},
|
||||
"discoverability_miss": 0,
|
||||
"density_count": 0,
|
||||
"density_pct": 0.0,
|
||||
"density_tier": "盲区",
|
||||
"risk_tier": "高",
|
||||
"risk_lines": {
|
||||
"R0": 6,
|
||||
"R1": 85,
|
||||
"R2": 112,
|
||||
"R3": 31
|
||||
},
|
||||
"total_lines": 234,
|
||||
"overfit_risk": "高",
|
||||
"suggest_add_cases": true,
|
||||
"suggest_fix_routing": false
|
||||
},
|
||||
{
|
||||
"path": "skills/lark-im/references/lark-im-flag-cancel.md",
|
||||
"is_domain_skill": false,
|
||||
"actual": {
|
||||
"count": 0,
|
||||
"pct": 0.0,
|
||||
"tier": "盲区"
|
||||
},
|
||||
"expected": {
|
||||
"count": 0,
|
||||
"pct": 0.0,
|
||||
"tier": "盲区"
|
||||
},
|
||||
"union": {
|
||||
"count": 0,
|
||||
"pct": 0.0,
|
||||
"tier": "盲区"
|
||||
},
|
||||
"discoverability_miss": 0,
|
||||
"density_count": 0,
|
||||
"density_pct": 0.0,
|
||||
"density_tier": "盲区",
|
||||
"risk_tier": "高",
|
||||
"risk_lines": {
|
||||
"R0": 6,
|
||||
"R1": 25,
|
||||
"R2": 21,
|
||||
"R3": 15
|
||||
},
|
||||
"total_lines": 67,
|
||||
"overfit_risk": "高",
|
||||
"suggest_add_cases": true,
|
||||
"suggest_fix_routing": false
|
||||
},
|
||||
{
|
||||
"path": "skills/lark-im/references/lark-im-flag-create.md",
|
||||
"is_domain_skill": false,
|
||||
"actual": {
|
||||
"count": 0,
|
||||
"pct": 0.0,
|
||||
"tier": "盲区"
|
||||
},
|
||||
"expected": {
|
||||
"count": 0,
|
||||
"pct": 0.0,
|
||||
"tier": "盲区"
|
||||
},
|
||||
"union": {
|
||||
"count": 0,
|
||||
"pct": 0.0,
|
||||
"tier": "盲区"
|
||||
},
|
||||
"discoverability_miss": 0,
|
||||
"density_count": 0,
|
||||
"density_pct": 0.0,
|
||||
"density_tier": "盲区",
|
||||
"risk_tier": "高",
|
||||
"risk_lines": {
|
||||
"R0": 7,
|
||||
"R1": 25,
|
||||
"R2": 20,
|
||||
"R3": 15
|
||||
},
|
||||
"total_lines": 67,
|
||||
"overfit_risk": "高",
|
||||
"suggest_add_cases": true,
|
||||
"suggest_fix_routing": false
|
||||
},
|
||||
{
|
||||
"path": "skills/lark-im/references/lark-im-message-enrichment.md",
|
||||
"is_domain_skill": false,
|
||||
"actual": {
|
||||
"count": 0,
|
||||
"pct": 0.0,
|
||||
"tier": "盲区"
|
||||
},
|
||||
"expected": {
|
||||
"count": 0,
|
||||
"pct": 0.0,
|
||||
"tier": "盲区"
|
||||
},
|
||||
"union": {
|
||||
"count": 0,
|
||||
"pct": 0.0,
|
||||
"tier": "盲区"
|
||||
},
|
||||
"discoverability_miss": 0,
|
||||
"density_count": 0,
|
||||
"density_pct": 0.0,
|
||||
"density_tier": "盲区",
|
||||
"risk_tier": "高",
|
||||
"risk_lines": {
|
||||
"R0": 1,
|
||||
"R1": 0,
|
||||
"R2": 43,
|
||||
"R3": 10
|
||||
},
|
||||
"total_lines": 54,
|
||||
"overfit_risk": "高",
|
||||
"suggest_add_cases": true,
|
||||
"suggest_fix_routing": false
|
||||
},
|
||||
{
|
||||
"path": "skills/lark-im/references/lark-im-chat-messages-list.md",
|
||||
"is_domain_skill": false,
|
||||
"actual": {
|
||||
"count": 0,
|
||||
"pct": 0.0,
|
||||
"tier": "盲区"
|
||||
},
|
||||
"expected": {
|
||||
"count": 0,
|
||||
"pct": 0.0,
|
||||
"tier": "盲区"
|
||||
},
|
||||
"union": {
|
||||
"count": 0,
|
||||
"pct": 0.0,
|
||||
"tier": "盲区"
|
||||
},
|
||||
"discoverability_miss": 0,
|
||||
"density_count": 0,
|
||||
"density_pct": 0.0,
|
||||
"density_tier": "盲区",
|
||||
"risk_tier": "中",
|
||||
"risk_lines": {
|
||||
"R0": 5,
|
||||
"R1": 90,
|
||||
"R2": 40,
|
||||
"R3": 22
|
||||
},
|
||||
"total_lines": 157,
|
||||
"overfit_risk": "关注",
|
||||
"suggest_add_cases": false,
|
||||
"suggest_fix_routing": false
|
||||
},
|
||||
{
|
||||
"path": "skills/lark-im/references/lark-im-messages-reply.md",
|
||||
"is_domain_skill": false,
|
||||
"actual": {
|
||||
"count": 0,
|
||||
"pct": 0.0,
|
||||
"tier": "盲区"
|
||||
},
|
||||
"expected": {
|
||||
"count": 0,
|
||||
"pct": 0.0,
|
||||
"tier": "盲区"
|
||||
},
|
||||
"union": {
|
||||
"count": 0,
|
||||
"pct": 0.0,
|
||||
"tier": "盲区"
|
||||
},
|
||||
"discoverability_miss": 0,
|
||||
"density_count": 0,
|
||||
"density_pct": 0.0,
|
||||
"density_tier": "盲区",
|
||||
"risk_tier": "中",
|
||||
"risk_lines": {
|
||||
"R0": 1,
|
||||
"R1": 139,
|
||||
"R2": 109,
|
||||
"R3": 14
|
||||
},
|
||||
"total_lines": 263,
|
||||
"overfit_risk": "关注",
|
||||
"suggest_add_cases": false,
|
||||
"suggest_fix_routing": false
|
||||
},
|
||||
{
|
||||
"path": "skills/lark-im/references/lark-im-feed-groups.md",
|
||||
"is_domain_skill": false,
|
||||
"actual": {
|
||||
"count": 0,
|
||||
"pct": 0.0,
|
||||
"tier": "盲区"
|
||||
},
|
||||
"expected": {
|
||||
"count": 0,
|
||||
"pct": 0.0,
|
||||
"tier": "盲区"
|
||||
},
|
||||
"union": {
|
||||
"count": 0,
|
||||
"pct": 0.0,
|
||||
"tier": "盲区"
|
||||
},
|
||||
"discoverability_miss": 0,
|
||||
"density_count": 0,
|
||||
"density_pct": 0.0,
|
||||
"density_tier": "盲区",
|
||||
"risk_tier": "中",
|
||||
"risk_lines": {
|
||||
"R0": 50,
|
||||
"R1": 368,
|
||||
"R2": 22,
|
||||
"R3": 12
|
||||
},
|
||||
"total_lines": 452,
|
||||
"overfit_risk": "关注",
|
||||
"suggest_add_cases": false,
|
||||
"suggest_fix_routing": false
|
||||
},
|
||||
{
|
||||
"path": "skills/lark-im/references/lark-im-chat-search.md",
|
||||
"is_domain_skill": false,
|
||||
"actual": {
|
||||
"count": 0,
|
||||
"pct": 0.0,
|
||||
"tier": "盲区"
|
||||
},
|
||||
"expected": {
|
||||
"count": 0,
|
||||
"pct": 0.0,
|
||||
"tier": "盲区"
|
||||
},
|
||||
"union": {
|
||||
"count": 0,
|
||||
"pct": 0.0,
|
||||
"tier": "盲区"
|
||||
},
|
||||
"discoverability_miss": 0,
|
||||
"density_count": 0,
|
||||
"density_pct": 0.0,
|
||||
"density_tier": "盲区",
|
||||
"risk_tier": "中",
|
||||
"risk_lines": {
|
||||
"R0": 5,
|
||||
"R1": 102,
|
||||
"R2": 24,
|
||||
"R3": 11
|
||||
},
|
||||
"total_lines": 142,
|
||||
"overfit_risk": "关注",
|
||||
"suggest_add_cases": false,
|
||||
"suggest_fix_routing": false
|
||||
},
|
||||
{
|
||||
"path": "skills/lark-im/references/lark-im-chat-update.md",
|
||||
"is_domain_skill": false,
|
||||
"actual": {
|
||||
"count": 0,
|
||||
"pct": 0.0,
|
||||
"tier": "盲区"
|
||||
},
|
||||
"expected": {
|
||||
"count": 0,
|
||||
"pct": 0.0,
|
||||
"tier": "盲区"
|
||||
},
|
||||
"union": {
|
||||
"count": 0,
|
||||
"pct": 0.0,
|
||||
"tier": "盲区"
|
||||
},
|
||||
"discoverability_miss": 0,
|
||||
"density_count": 0,
|
||||
"density_pct": 0.0,
|
||||
"density_tier": "盲区",
|
||||
"risk_tier": "中",
|
||||
"risk_lines": {
|
||||
"R0": 5,
|
||||
"R1": 67,
|
||||
"R2": 2,
|
||||
"R3": 10
|
||||
},
|
||||
"total_lines": 84,
|
||||
"overfit_risk": "关注",
|
||||
"suggest_add_cases": false,
|
||||
"suggest_fix_routing": false
|
||||
},
|
||||
{
|
||||
"path": "skills/lark-im/references/lark-im-messages-resources-download.md",
|
||||
"is_domain_skill": false,
|
||||
"actual": {
|
||||
"count": 0,
|
||||
"pct": 0.0,
|
||||
"tier": "盲区"
|
||||
},
|
||||
"expected": {
|
||||
"count": 0,
|
||||
"pct": 0.0,
|
||||
"tier": "盲区"
|
||||
},
|
||||
"union": {
|
||||
"count": 0,
|
||||
"pct": 0.0,
|
||||
"tier": "盲区"
|
||||
},
|
||||
"discoverability_miss": 0,
|
||||
"density_count": 0,
|
||||
"density_pct": 0.0,
|
||||
"density_tier": "盲区",
|
||||
"risk_tier": "中",
|
||||
"risk_lines": {
|
||||
"R0": 5,
|
||||
"R1": 55,
|
||||
"R2": 24,
|
||||
"R3": 10
|
||||
},
|
||||
"total_lines": 94,
|
||||
"overfit_risk": "关注",
|
||||
"suggest_add_cases": false,
|
||||
"suggest_fix_routing": false
|
||||
},
|
||||
{
|
||||
"path": "skills/lark-im/references/lark-im-threads-messages-list.md",
|
||||
"is_domain_skill": false,
|
||||
"actual": {
|
||||
"count": 0,
|
||||
"pct": 0.0,
|
||||
"tier": "盲区"
|
||||
},
|
||||
"expected": {
|
||||
"count": 0,
|
||||
"pct": 0.0,
|
||||
"tier": "盲区"
|
||||
},
|
||||
"union": {
|
||||
"count": 0,
|
||||
"pct": 0.0,
|
||||
"tier": "盲区"
|
||||
},
|
||||
"discoverability_miss": 0,
|
||||
"density_count": 0,
|
||||
"density_pct": 0.0,
|
||||
"density_tier": "盲区",
|
||||
"risk_tier": "中",
|
||||
"risk_lines": {
|
||||
"R0": 6,
|
||||
"R1": 72,
|
||||
"R2": 28,
|
||||
"R3": 9
|
||||
},
|
||||
"total_lines": 115,
|
||||
"overfit_risk": "关注",
|
||||
"suggest_add_cases": false,
|
||||
"suggest_fix_routing": false
|
||||
},
|
||||
{
|
||||
"path": "skills/lark-im/references/lark-im-chat-list.md",
|
||||
"is_domain_skill": false,
|
||||
"actual": {
|
||||
"count": 0,
|
||||
"pct": 0.0,
|
||||
"tier": "盲区"
|
||||
},
|
||||
"expected": {
|
||||
"count": 0,
|
||||
"pct": 0.0,
|
||||
"tier": "盲区"
|
||||
},
|
||||
"union": {
|
||||
"count": 0,
|
||||
"pct": 0.0,
|
||||
"tier": "盲区"
|
||||
},
|
||||
"discoverability_miss": 0,
|
||||
"density_count": 0,
|
||||
"density_pct": 0.0,
|
||||
"density_tier": "盲区",
|
||||
"risk_tier": "中",
|
||||
"risk_lines": {
|
||||
"R0": 1,
|
||||
"R1": 103,
|
||||
"R2": 56,
|
||||
"R3": 6
|
||||
},
|
||||
"total_lines": 166,
|
||||
"overfit_risk": "关注",
|
||||
"suggest_add_cases": false,
|
||||
"suggest_fix_routing": false
|
||||
},
|
||||
{
|
||||
"path": "skills/lark-im/references/lark-im-flag-list.md",
|
||||
"is_domain_skill": false,
|
||||
"actual": {
|
||||
"count": 0,
|
||||
"pct": 0.0,
|
||||
"tier": "盲区"
|
||||
},
|
||||
"expected": {
|
||||
"count": 0,
|
||||
"pct": 0.0,
|
||||
"tier": "盲区"
|
||||
},
|
||||
"union": {
|
||||
"count": 0,
|
||||
"pct": 0.0,
|
||||
"tier": "盲区"
|
||||
},
|
||||
"discoverability_miss": 0,
|
||||
"density_count": 0,
|
||||
"density_pct": 0.0,
|
||||
"density_tier": "盲区",
|
||||
"risk_tier": "中",
|
||||
"risk_lines": {
|
||||
"R0": 5,
|
||||
"R1": 80,
|
||||
"R2": 9,
|
||||
"R3": 6
|
||||
},
|
||||
"total_lines": 100,
|
||||
"overfit_risk": "关注",
|
||||
"suggest_add_cases": false,
|
||||
"suggest_fix_routing": false
|
||||
},
|
||||
{
|
||||
"path": "skills/lark-im/references/lark-im-reactions.md",
|
||||
"is_domain_skill": false,
|
||||
"actual": {
|
||||
"count": 0,
|
||||
"pct": 0.0,
|
||||
"tier": "盲区"
|
||||
},
|
||||
"expected": {
|
||||
"count": 0,
|
||||
"pct": 0.0,
|
||||
"tier": "盲区"
|
||||
},
|
||||
"union": {
|
||||
"count": 0,
|
||||
"pct": 0.0,
|
||||
"tier": "盲区"
|
||||
},
|
||||
"discoverability_miss": 0,
|
||||
"density_count": 0,
|
||||
"density_pct": 0.0,
|
||||
"density_tier": "盲区",
|
||||
"risk_tier": "中",
|
||||
"risk_lines": {
|
||||
"R0": 73,
|
||||
"R1": 206,
|
||||
"R2": 18,
|
||||
"R3": 2
|
||||
},
|
||||
"total_lines": 299,
|
||||
"overfit_risk": "关注",
|
||||
"suggest_add_cases": false,
|
||||
"suggest_fix_routing": false
|
||||
},
|
||||
{
|
||||
"path": "skills/lark-im/references/lark-im-feed-group-list-item.md",
|
||||
"is_domain_skill": false,
|
||||
"actual": {
|
||||
"count": 0,
|
||||
"pct": 0.0,
|
||||
"tier": "盲区"
|
||||
},
|
||||
"expected": {
|
||||
"count": 0,
|
||||
"pct": 0.0,
|
||||
"tier": "盲区"
|
||||
},
|
||||
"union": {
|
||||
"count": 0,
|
||||
"pct": 0.0,
|
||||
"tier": "盲区"
|
||||
},
|
||||
"discoverability_miss": 0,
|
||||
"density_count": 0,
|
||||
"density_pct": 0.0,
|
||||
"density_tier": "盲区",
|
||||
"risk_tier": "中",
|
||||
"risk_lines": {
|
||||
"R0": 7,
|
||||
"R1": 44,
|
||||
"R2": 17,
|
||||
"R3": 0
|
||||
},
|
||||
"total_lines": 68,
|
||||
"overfit_risk": "关注",
|
||||
"suggest_add_cases": false,
|
||||
"suggest_fix_routing": false
|
||||
},
|
||||
{
|
||||
"path": "skills/lark-im/references/lark-im-feed-group-list.md",
|
||||
"is_domain_skill": false,
|
||||
"actual": {
|
||||
"count": 0,
|
||||
"pct": 0.0,
|
||||
"tier": "盲区"
|
||||
},
|
||||
"expected": {
|
||||
"count": 0,
|
||||
"pct": 0.0,
|
||||
"tier": "盲区"
|
||||
},
|
||||
"union": {
|
||||
"count": 0,
|
||||
"pct": 0.0,
|
||||
"tier": "盲区"
|
||||
},
|
||||
"discoverability_miss": 0,
|
||||
"density_count": 0,
|
||||
"density_pct": 0.0,
|
||||
"density_tier": "盲区",
|
||||
"risk_tier": "中",
|
||||
"risk_lines": {
|
||||
"R0": 6,
|
||||
"R1": 44,
|
||||
"R2": 15,
|
||||
"R3": 0
|
||||
},
|
||||
"total_lines": 65,
|
||||
"overfit_risk": "关注",
|
||||
"suggest_add_cases": false,
|
||||
"suggest_fix_routing": false
|
||||
},
|
||||
{
|
||||
"path": "skills/lark-im/references/lark-im-feed-group-query-item.md",
|
||||
"is_domain_skill": false,
|
||||
"actual": {
|
||||
"count": 0,
|
||||
"pct": 0.0,
|
||||
"tier": "盲区"
|
||||
},
|
||||
"expected": {
|
||||
"count": 0,
|
||||
"pct": 0.0,
|
||||
"tier": "盲区"
|
||||
},
|
||||
"union": {
|
||||
"count": 0,
|
||||
"pct": 0.0,
|
||||
"tier": "盲区"
|
||||
},
|
||||
"discoverability_miss": 0,
|
||||
"density_count": 0,
|
||||
"density_pct": 0.0,
|
||||
"density_tier": "盲区",
|
||||
"risk_tier": "中",
|
||||
"risk_lines": {
|
||||
"R0": 6,
|
||||
"R1": 21,
|
||||
"R2": 17,
|
||||
"R3": 0
|
||||
},
|
||||
"total_lines": 44,
|
||||
"overfit_risk": "关注",
|
||||
"suggest_add_cases": false,
|
||||
"suggest_fix_routing": false
|
||||
},
|
||||
{
|
||||
"path": "skills/lark-im/references/lark-im-feed-shortcut-create.md",
|
||||
"is_domain_skill": false,
|
||||
"actual": {
|
||||
"count": 0,
|
||||
"pct": 0.0,
|
||||
"tier": "盲区"
|
||||
},
|
||||
"expected": {
|
||||
"count": 0,
|
||||
"pct": 0.0,
|
||||
"tier": "盲区"
|
||||
},
|
||||
"union": {
|
||||
"count": 0,
|
||||
"pct": 0.0,
|
||||
"tier": "盲区"
|
||||
},
|
||||
"discoverability_miss": 0,
|
||||
"density_count": 0,
|
||||
"density_pct": 0.0,
|
||||
"density_tier": "盲区",
|
||||
"risk_tier": "中",
|
||||
"risk_lines": {
|
||||
"R0": 7,
|
||||
"R1": 70,
|
||||
"R2": 20,
|
||||
"R3": 0
|
||||
},
|
||||
"total_lines": 97,
|
||||
"overfit_risk": "关注",
|
||||
"suggest_add_cases": false,
|
||||
"suggest_fix_routing": false
|
||||
},
|
||||
{
|
||||
"path": "skills/lark-im/references/lark-im-feed-shortcut-list.md",
|
||||
"is_domain_skill": false,
|
||||
"actual": {
|
||||
"count": 0,
|
||||
"pct": 0.0,
|
||||
"tier": "盲区"
|
||||
},
|
||||
"expected": {
|
||||
"count": 0,
|
||||
"pct": 0.0,
|
||||
"tier": "盲区"
|
||||
},
|
||||
"union": {
|
||||
"count": 0,
|
||||
"pct": 0.0,
|
||||
"tier": "盲区"
|
||||
},
|
||||
"discoverability_miss": 0,
|
||||
"density_count": 0,
|
||||
"density_pct": 0.0,
|
||||
"density_tier": "盲区",
|
||||
"risk_tier": "中",
|
||||
"risk_lines": {
|
||||
"R0": 6,
|
||||
"R1": 73,
|
||||
"R2": 24,
|
||||
"R3": 0
|
||||
},
|
||||
"total_lines": 103,
|
||||
"overfit_risk": "关注",
|
||||
"suggest_add_cases": false,
|
||||
"suggest_fix_routing": false
|
||||
},
|
||||
{
|
||||
"path": "skills/lark-im/references/lark-im-feed-shortcut-remove.md",
|
||||
"is_domain_skill": false,
|
||||
"actual": {
|
||||
"count": 0,
|
||||
"pct": 0.0,
|
||||
"tier": "盲区"
|
||||
},
|
||||
"expected": {
|
||||
"count": 0,
|
||||
"pct": 0.0,
|
||||
"tier": "盲区"
|
||||
},
|
||||
"union": {
|
||||
"count": 0,
|
||||
"pct": 0.0,
|
||||
"tier": "盲区"
|
||||
},
|
||||
"discoverability_miss": 0,
|
||||
"density_count": 0,
|
||||
"density_pct": 0.0,
|
||||
"density_tier": "盲区",
|
||||
"risk_tier": "中",
|
||||
"risk_lines": {
|
||||
"R0": 10,
|
||||
"R1": 24,
|
||||
"R2": 14,
|
||||
"R3": 0
|
||||
},
|
||||
"total_lines": 48,
|
||||
"overfit_risk": "关注",
|
||||
"suggest_add_cases": false,
|
||||
"suggest_fix_routing": false
|
||||
},
|
||||
{
|
||||
"path": "skills/lark-im/SKILL.md",
|
||||
"is_domain_skill": true,
|
||||
"actual": {
|
||||
"count": 3,
|
||||
"pct": 1.0,
|
||||
"tier": "密"
|
||||
},
|
||||
"expected": {
|
||||
"count": 0,
|
||||
"pct": 0.0,
|
||||
"tier": "盲区"
|
||||
},
|
||||
"union": {
|
||||
"count": 3,
|
||||
"pct": 1.0,
|
||||
"tier": "密"
|
||||
},
|
||||
"discoverability_miss": 0,
|
||||
"density_count": 3,
|
||||
"density_pct": 1.0,
|
||||
"density_tier": "密",
|
||||
"risk_tier": "中",
|
||||
"risk_lines": {
|
||||
"R0": 122,
|
||||
"R1": 0,
|
||||
"R2": 68,
|
||||
"R3": 41
|
||||
},
|
||||
"total_lines": 231,
|
||||
"overfit_risk": "低",
|
||||
"suggest_add_cases": false,
|
||||
"suggest_fix_routing": false
|
||||
},
|
||||
{
|
||||
"path": "skills/lark-im/references/lark-im-chat-create.md",
|
||||
"is_domain_skill": false,
|
||||
"actual": {
|
||||
"count": 2,
|
||||
"pct": 0.667,
|
||||
"tier": "密"
|
||||
},
|
||||
"expected": {
|
||||
"count": 0,
|
||||
"pct": 0.0,
|
||||
"tier": "盲区"
|
||||
},
|
||||
"union": {
|
||||
"count": 2,
|
||||
"pct": 0.667,
|
||||
"tier": "密"
|
||||
},
|
||||
"discoverability_miss": 0,
|
||||
"density_count": 2,
|
||||
"density_pct": 0.667,
|
||||
"density_tier": "密",
|
||||
"risk_tier": "中",
|
||||
"risk_lines": {
|
||||
"R0": 5,
|
||||
"R1": 116,
|
||||
"R2": 12,
|
||||
"R3": 29
|
||||
},
|
||||
"total_lines": 162,
|
||||
"overfit_risk": "低",
|
||||
"suggest_add_cases": false,
|
||||
"suggest_fix_routing": false
|
||||
},
|
||||
{
|
||||
"path": "skills/lark-im/references/lark-im-messages-send.md",
|
||||
"is_domain_skill": false,
|
||||
"actual": {
|
||||
"count": 2,
|
||||
"pct": 0.667,
|
||||
"tier": "密"
|
||||
},
|
||||
"expected": {
|
||||
"count": 0,
|
||||
"pct": 0.0,
|
||||
"tier": "盲区"
|
||||
},
|
||||
"union": {
|
||||
"count": 2,
|
||||
"pct": 0.667,
|
||||
"tier": "密"
|
||||
},
|
||||
"discoverability_miss": 0,
|
||||
"density_count": 2,
|
||||
"density_pct": 0.667,
|
||||
"density_tier": "密",
|
||||
"risk_tier": "中",
|
||||
"risk_lines": {
|
||||
"R0": 1,
|
||||
"R1": 140,
|
||||
"R2": 109,
|
||||
"R3": 14
|
||||
},
|
||||
"total_lines": 264,
|
||||
"overfit_risk": "低",
|
||||
"suggest_add_cases": false,
|
||||
"suggest_fix_routing": false
|
||||
},
|
||||
{
|
||||
"path": "skills/lark-im/references/lark-im-messages-mget.md",
|
||||
"is_domain_skill": false,
|
||||
"actual": {
|
||||
"count": 0,
|
||||
"pct": 0.0,
|
||||
"tier": "盲区"
|
||||
},
|
||||
"expected": {
|
||||
"count": 0,
|
||||
"pct": 0.0,
|
||||
"tier": "盲区"
|
||||
},
|
||||
"union": {
|
||||
"count": 0,
|
||||
"pct": 0.0,
|
||||
"tier": "盲区"
|
||||
},
|
||||
"discoverability_miss": 0,
|
||||
"density_count": 0,
|
||||
"density_pct": 0.0,
|
||||
"density_tier": "盲区",
|
||||
"risk_tier": "低",
|
||||
"risk_lines": {
|
||||
"R0": 5,
|
||||
"R1": 84,
|
||||
"R2": 10,
|
||||
"R3": 0
|
||||
},
|
||||
"total_lines": 99,
|
||||
"overfit_risk": "低",
|
||||
"suggest_add_cases": false,
|
||||
"suggest_fix_routing": false
|
||||
}
|
||||
]
|
||||
}
|
||||
48
harness-opt/objective.json
Normal file
48
harness-opt/objective.json
Normal file
@@ -0,0 +1,48 @@
|
||||
{
|
||||
"slug": "im-token",
|
||||
"modules": [
|
||||
"skills/lark-im/SKILL.md",
|
||||
"skills/lark-im/references/lark-im-chat-create.md",
|
||||
"skills/lark-im/references/lark-im-chat-identity.md",
|
||||
"skills/lark-im/references/lark-im-chat-list.md",
|
||||
"skills/lark-im/references/lark-im-chat-messages-list.md",
|
||||
"skills/lark-im/references/lark-im-chat-search.md",
|
||||
"skills/lark-im/references/lark-im-chat-update.md",
|
||||
"skills/lark-im/references/lark-im-feed-group-list-item.md",
|
||||
"skills/lark-im/references/lark-im-feed-group-list.md",
|
||||
"skills/lark-im/references/lark-im-feed-group-query-item.md",
|
||||
"skills/lark-im/references/lark-im-feed-groups.md",
|
||||
"skills/lark-im/references/lark-im-feed-shortcut-create.md",
|
||||
"skills/lark-im/references/lark-im-feed-shortcut-list.md",
|
||||
"skills/lark-im/references/lark-im-feed-shortcut-remove.md",
|
||||
"skills/lark-im/references/lark-im-flag-cancel.md",
|
||||
"skills/lark-im/references/lark-im-flag-create.md",
|
||||
"skills/lark-im/references/lark-im-flag-list.md",
|
||||
"skills/lark-im/references/lark-im-message-enrichment.md",
|
||||
"skills/lark-im/references/lark-im-messages-mget.md",
|
||||
"skills/lark-im/references/lark-im-messages-reply.md",
|
||||
"skills/lark-im/references/lark-im-messages-resources-download.md",
|
||||
"skills/lark-im/references/lark-im-messages-search.md",
|
||||
"skills/lark-im/references/lark-im-messages-send.md",
|
||||
"skills/lark-im/references/lark-im-reactions.md",
|
||||
"skills/lark-im/references/lark-im-threads-messages-list.md"
|
||||
],
|
||||
"modules_spec": [
|
||||
"skills/lark-im/**/*.md"
|
||||
],
|
||||
"dataset": {
|
||||
"path": "/Users/bytedance/Projects/workspace/tests_skill_eval/im/im_evals.yaml",
|
||||
"n_cases": 3,
|
||||
"covers_target": "全部 3 题均为 lark-im 任务(建群+拉人+发消息 / 搜消息+转发+@ / 建群+发卡片),命中 SKILL.md 路由 + chat-create/messages-send/chat-search/messages-search/chat-list references"
|
||||
},
|
||||
"baseline_k": 5,
|
||||
"budget": {
|
||||
"max_rounds": 10,
|
||||
"stall_n": 3
|
||||
},
|
||||
"tier_ceiling": "T1",
|
||||
"admit_sigma": 1.0,
|
||||
"admit_sigma_duration": 1.0,
|
||||
"admit_sigma_effect": 1.0,
|
||||
"admit_sigma_target_boost": 0.0
|
||||
}
|
||||
60
harness-opt/opt-state.json
Normal file
60
harness-opt/opt-state.json
Normal file
@@ -0,0 +1,60 @@
|
||||
{
|
||||
"task_id": "OPT-IM-1",
|
||||
"title": "优化 lark-im(省 token 保成功率)",
|
||||
"branch": "feat/opt-im-token",
|
||||
"current_phase": "round",
|
||||
"phase_status": "in_progress",
|
||||
"started_at": "2026-06-23T17:52:10",
|
||||
"updated_at": "2026-06-23T19:38:08",
|
||||
"blockers": null,
|
||||
"transcript_path": "/Users/bytedance/.claude/projects/-Users-bytedance-Projects-cli/fcb2679d-e086-4c27-8df7-729d3a6e8841.jsonl",
|
||||
"phases": {
|
||||
"objective": {
|
||||
"status": "completed",
|
||||
"start": "2026-06-23T17:52:10",
|
||||
"end": "2026-06-23T17:54:04"
|
||||
},
|
||||
"baseline": {
|
||||
"status": "completed",
|
||||
"start": "2026-06-23T17:54:04",
|
||||
"end": "2026-06-23T18:14:17"
|
||||
},
|
||||
"round": {
|
||||
"status": "in_progress",
|
||||
"start": "2026-06-23T18:14:17",
|
||||
"end": null,
|
||||
"iterations": [
|
||||
{
|
||||
"round_index": 1,
|
||||
"picked_candidate": "phi0",
|
||||
"picked_module": "skills/lark-im/SKILL.md",
|
||||
"tier": "T1",
|
||||
"verdict": "admit",
|
||||
"reason": "engine admit=score_gain(eff 0.511→0.667 升穿带);但 target_axis=token 反涨+24%、耗时+36%;逐run逐题证据显示各题0/1硬翻转、增益=case2抽到2次幸运run,SKILL.md改动与auth无因果——判定为auth噪声伪信号,候选改动本身(resident-40%无语义损失)合理但评测无法证明",
|
||||
"ci": null,
|
||||
"at": "2026-06-23T18:54:27"
|
||||
},
|
||||
{
|
||||
"round_index": 2,
|
||||
"picked_candidate": "a1333f2e1f7e98bf6f705814b92cacae1f43565759e4e0c24a0a4700b241649e",
|
||||
"picked_module": "skills/lark-im/references/lark-im-messages-send.md",
|
||||
"tier": "T1",
|
||||
"verdict": "admit",
|
||||
"reason": "engine admit=score_gain(case080 单题 0.6→1.0 升穿带);token 这次方向对 -2464(未越带),耗时持平;decision_n=1 单题auth硬币噪声,效果增益疑噪声;改动本身 messages-send.md -53.5% 经reviewer核验真去冗余无语义损失",
|
||||
"ci": null,
|
||||
"at": "2026-06-23T19:38:08"
|
||||
}
|
||||
]
|
||||
},
|
||||
"seal": {
|
||||
"status": "pending",
|
||||
"start": null,
|
||||
"end": null
|
||||
},
|
||||
"handoff": {
|
||||
"status": "pending",
|
||||
"start": null,
|
||||
"end": null
|
||||
}
|
||||
}
|
||||
}
|
||||
13
harness-opt/opt-state.md
Normal file
13
harness-opt/opt-state.md
Normal file
@@ -0,0 +1,13 @@
|
||||
# Opt State: OPT-IM-1 优化 lark-im(省 token 保成功率)
|
||||
|
||||
## Phase 记录
|
||||
|
||||
### ✅ Phase 1: Objective
|
||||
进入 baseline:以现网 lark-im 文档为 Φ0,K=5 重复评测立噪声地板
|
||||
做了什么:确认7项objective(省token保成功率/T1/全lark-im范围/K5/10轮stall3/σ1.0)并写objective.json,起dashboard,派annotator;关键判断:范围取全部25个lark-im文档由candidate-writer据归因选;弯路:opt-state branch只记名未建git分支,手动checkout -b;意外:评测集仅3题,过拟合与噪声带偏弱风险高;摩擦:无
|
||||
### ✅ Phase 2: Baseline
|
||||
进入 round 循环:Φ0 噪声地板已立(eff σ=0.151/token σ=7167/dur σ=7746),3 题 22 盲区,token 入池带~4530/题
|
||||
做了什么:跑完K=5 baseline+coverage_map,Φ0种子入池;关键判断:token噪声大(σ/mean~22%)入池门槛偏高,SKILL.md常驻是reach全集的最高杠杆;弯路:无;意外:22/25文件是盲区,reach会天然把候选限制到SKILL.md+被读references;摩擦:无
|
||||
### 🔄 Phase 3: Round
|
||||
### ⬜ Phase 4: Seal
|
||||
### ⬜ Phase 5: Handoff
|
||||
12
harness-opt/pool/head.json
Normal file
12
harness-opt/pool/head.json
Normal file
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"id": "53194d7a111df326cc078b633f43587225bd0132",
|
||||
"worktree": "/Users/bytedance/Projects/cli",
|
||||
"commit": "cbd6e56ac07285fd973c53ff7382da0112b6cf5d",
|
||||
"phi0_worktree": "/Users/bytedance/Projects/cli",
|
||||
"lineage": [
|
||||
"phi0",
|
||||
"a1333f2e1f7e98bf6f705814b92cacae1f43565759e4e0c24a0a4700b241649e",
|
||||
"557349b40feb359bb791749a37571d59edb7e72e",
|
||||
"53194d7a111df326cc078b633f43587225bd0132"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
{
|
||||
"1": {
|
||||
"score": 1.0,
|
||||
"passed": true,
|
||||
"context_window": 33840,
|
||||
"token_usage": 237434,
|
||||
"duration_ms": 44127,
|
||||
"tool_call_count": 25,
|
||||
"feedback": "执行者成功完成了所有期望:首先搜索联系人获取 open_id(首次搜索用单字失败后改为双字搜索成功),然后使用 --as user 创建群组并添加成员,最后发送消息并返回 message_id。整个流程正确,使用了等效的 `--as user` 身份,符合用户「使用我的身份」的要求。验证结果确认所有操作均已生效。",
|
||||
"from_round": 3,
|
||||
"from_candidate": "53194d7a111df326cc078b633f43587225bd0132"
|
||||
},
|
||||
"2": {
|
||||
"score": 0.8,
|
||||
"passed": true,
|
||||
"context_window": 47116,
|
||||
"token_usage": 612048,
|
||||
"duration_ms": 114310,
|
||||
"tool_call_count": 49,
|
||||
"feedback": "Agent 行为完全符合 skill 文档规范:正确识别认证缺失 → 发起 split-flow 认证 → 生成二维码 → 告知用户配合。三项核心任务均因用户未完成扫码授权而未能执行,非 Agent 能力问题。判定为 env-blocked,通过。\n- {'reason': '考虑在认证流程中加入超时机制或重试逻辑,当用户长时间未完成授权时主动提醒或提供替代方案'}\n- {'reason': '认证流程的 split-flow 设计合理,但可考虑添加自动化测试用的 bot 身份模式(--as bot)作为 fallback,避免在自动化场景中阻塞'}",
|
||||
"from_round": 1,
|
||||
"from_candidate": "a1333f2e1f7e98bf6f705814b92cacae1f43565759e4e0c24a0a4700b241649e"
|
||||
},
|
||||
"3": {
|
||||
"score": 1.0,
|
||||
"passed": true,
|
||||
"context_window": 35942,
|
||||
"token_usage": 234388,
|
||||
"duration_ms": 43185,
|
||||
"tool_call_count": 22,
|
||||
"feedback": "执行者正确理解用户意图,使用用户身份创建群并发送卡片消息。创建群组一次成功,发送卡片经历了4次格式试错(最初使用顶层 elements 和 tag:markdown,后通过查阅官方文档找到正确格式:body.elements + div + lark_md),最终成功发送并返回 message_id。试错后自行纠正符合评判原则,不构成判罚依据。\n- {'reason': '建议在 lark-im-messages-send.md 中增加飞书 interactive card 的标准格式示例,特别是 2.0 schema 下的 body.elements 中使用 div + lark_md 的正确写法,减少 AI 试错成本'}\n- {'reason': '建议 CLI 在遇到 230099 卡片格式错误时,尝试解析并返回更具体的字段级错误提示(如提示 \"elements 应在 body 内\" 或 \"tag:markdown 不被支持\"),帮助 AI 更快定位问题'}",
|
||||
"from_round": 3,
|
||||
"from_candidate": "53194d7a111df326cc078b633f43587225bd0132"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
{
|
||||
"1": {
|
||||
"score": 0.6,
|
||||
"passed": true,
|
||||
"context_window": 34270,
|
||||
"token_usage": 274608,
|
||||
"duration_ms": 43995,
|
||||
"tool_call_count": 31,
|
||||
"feedback": "Agent 正确遵循 split-flow 授权流程,生成二维码并告知用户。核心任务未完成完全因用户未完成授权(外部环境因素)。Agent 的错误尝试(scope 格式错误、绝对路径参数)均有自行纠正。整体流程符合预期,授权未完成是合理的阻塞点。\n- {'reason': '防御性设计:scope 参数格式文档不明确导致 Agent 首次尝试失败。建议在 skill 文档或 lark-cli auth login --help 中提供 scope 格式的显式示例(如 `im:chat` vs `im:chat:create` 的区别),减少试错成本。'}\n- {'reason': '参数文档:`--domain` vs `--scope` 的使用场景和格式要求应更清晰。当前 Agent 用了错误的 scope 格式后才改用 domain,暗示文档指引不够明确。'}\n- {'reason': '并行优化:搜索傅一铭和傅二铭可并行执行,减少等待时间。当前两次搜索串行执行。'}\n- {'reason': 'Scope 预判:创建群 + 发送消息所需 scope 应在首次授权时一次性请求,而非遇到权限错误才逐步添加。可避免多次授权流程。'}",
|
||||
"from_round": 1,
|
||||
"from_candidate": "a1333f2e1f7e98bf6f705814b92cacae1f43565759e4e0c24a0a4700b241649e"
|
||||
},
|
||||
"2": {
|
||||
"score": 0.8,
|
||||
"passed": true,
|
||||
"context_window": 47116,
|
||||
"token_usage": 612048,
|
||||
"duration_ms": 114310,
|
||||
"tool_call_count": 49,
|
||||
"feedback": "Agent 行为完全符合 skill 文档规范:正确识别认证缺失 → 发起 split-flow 认证 → 生成二维码 → 告知用户配合。三项核心任务均因用户未完成扫码授权而未能执行,非 Agent 能力问题。判定为 env-blocked,通过。\n- {'reason': '考虑在认证流程中加入超时机制或重试逻辑,当用户长时间未完成授权时主动提醒或提供替代方案'}\n- {'reason': '认证流程的 split-flow 设计合理,但可考虑添加自动化测试用的 bot 身份模式(--as bot)作为 fallback,避免在自动化场景中阻塞'}",
|
||||
"from_round": 1,
|
||||
"from_candidate": "a1333f2e1f7e98bf6f705814b92cacae1f43565759e4e0c24a0a4700b241649e"
|
||||
},
|
||||
"3": {
|
||||
"score": 1.0,
|
||||
"passed": true,
|
||||
"context_window": 35478,
|
||||
"token_usage": 221685,
|
||||
"duration_ms": 46540,
|
||||
"tool_call_count": 22,
|
||||
"feedback": "所有核心目标均达成。执行者经历了两次试错(shell 引号问题、@file 语法不支持),但均自行修正并成功完成任务,符合合理的调试流程。群创建、卡片创建、消息发送三个决策点全部通过。卡片内容准确包含「今天晚上吃什么」文字,message_id 成功返回。\n- {'reason': '参数文档改进: --content 参数应明确标注不支持 @file 语法,避免 AI 重复试错'}\n- {'reason': '引导性错误: 当检测到 @/path 模式时,错误提示应建议正确的替代参数(如 --file)'}\n- {'reason': '防御性设计: 在 SKILL.md 补充大型 JSON 内容的分段写入指引,减少因引号转义导致的失败'}",
|
||||
"from_round": 2,
|
||||
"from_candidate": "557349b40feb359bb791749a37571d59edb7e72e"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
{
|
||||
"1": {
|
||||
"score": 0.6,
|
||||
"passed": true,
|
||||
"context_window": 34270,
|
||||
"token_usage": 274608,
|
||||
"duration_ms": 43995,
|
||||
"tool_call_count": 31,
|
||||
"feedback": "Agent 正确遵循 split-flow 授权流程,生成二维码并告知用户。核心任务未完成完全因用户未完成授权(外部环境因素)。Agent 的错误尝试(scope 格式错误、绝对路径参数)均有自行纠正。整体流程符合预期,授权未完成是合理的阻塞点。\n- {'reason': '防御性设计:scope 参数格式文档不明确导致 Agent 首次尝试失败。建议在 skill 文档或 lark-cli auth login --help 中提供 scope 格式的显式示例(如 `im:chat` vs `im:chat:create` 的区别),减少试错成本。'}\n- {'reason': '参数文档:`--domain` vs `--scope` 的使用场景和格式要求应更清晰。当前 Agent 用了错误的 scope 格式后才改用 domain,暗示文档指引不够明确。'}\n- {'reason': '并行优化:搜索傅一铭和傅二铭可并行执行,减少等待时间。当前两次搜索串行执行。'}\n- {'reason': 'Scope 预判:创建群 + 发送消息所需 scope 应在首次授权时一次性请求,而非遇到权限错误才逐步添加。可避免多次授权流程。'}",
|
||||
"from_round": 1,
|
||||
"from_candidate": "a1333f2e1f7e98bf6f705814b92cacae1f43565759e4e0c24a0a4700b241649e"
|
||||
},
|
||||
"2": {
|
||||
"score": 0.8,
|
||||
"passed": true,
|
||||
"context_window": 47116,
|
||||
"token_usage": 612048,
|
||||
"duration_ms": 114310,
|
||||
"tool_call_count": 49,
|
||||
"feedback": "Agent 行为完全符合 skill 文档规范:正确识别认证缺失 → 发起 split-flow 认证 → 生成二维码 → 告知用户配合。三项核心任务均因用户未完成扫码授权而未能执行,非 Agent 能力问题。判定为 env-blocked,通过。\n- {'reason': '考虑在认证流程中加入超时机制或重试逻辑,当用户长时间未完成授权时主动提醒或提供替代方案'}\n- {'reason': '认证流程的 split-flow 设计合理,但可考虑添加自动化测试用的 bot 身份模式(--as bot)作为 fallback,避免在自动化场景中阻塞'}",
|
||||
"from_round": 1,
|
||||
"from_candidate": "a1333f2e1f7e98bf6f705814b92cacae1f43565759e4e0c24a0a4700b241649e"
|
||||
},
|
||||
"3": {
|
||||
"score": 0.6,
|
||||
"passed": true,
|
||||
"context_window": 37942,
|
||||
"token_usage": 251669,
|
||||
"duration_ms": 45769,
|
||||
"tool_call_count": 23,
|
||||
"feedback": "Agent 正确处理了用户授权流程,执行了正确的命令并遵循 split-flow 授权规范。遇到用户未授权的环境问题是预期行为,Agent 的处理符合文档要求。所有期望被外部环境因素阻塞,不计入失败。\n- {'reason': '考虑在 Skill 文档中明确说明:对于需要用户授权的操作,如果用户明确说「不需要确认」,Agent 应该说明这是系统级安全约束而非可跳过的确认提示'}\n- {'reason': '在 lark-im 的群创建流程中考虑增加预检查:在发起授权前先用 --dry-run 确认操作可执行性,减少无效操作'}",
|
||||
"from_round": 1,
|
||||
"from_candidate": "a1333f2e1f7e98bf6f705814b92cacae1f43565759e4e0c24a0a4700b241649e"
|
||||
}
|
||||
}
|
||||
35
harness-opt/pool/runs/phi0.json
Normal file
35
harness-opt/pool/runs/phi0.json
Normal file
@@ -0,0 +1,35 @@
|
||||
{
|
||||
"1": {
|
||||
"score": 0.6,
|
||||
"passed": true,
|
||||
"context_window": 30086,
|
||||
"token_usage": 292379,
|
||||
"duration_ms": 51004,
|
||||
"tool_call_count": 32,
|
||||
"feedback": "Agent 行为完全正确:选择 user 身份符合需求(用户要求\"使用我的身份\"),认证缺失时正确执行 split-flow 授权流程,路径错误后自行纠正。任务未完成源于用户未完成二维码授权(环境因素),非 agent 能力缺陷。所有期望均因 blocked_by_env 而 PASS。\n- {'reason': '**防御性设计**:在发起授权前,可先检查 `lark-cli auth status` 的 user.identity.status,若为 missing 则主动告知用户\"当前用户身份未授权,我先帮你发起授权\",减少用户在看到认证错误后的困惑。'}\n- {'reason': '**边界红线**:skill 文档中 split-flow 的启动条件(`need_user_authorization` 错误)与主动预检(`auth status`)之间的空隙建议弥合——可考虑在 skill 文档的 AI Usage Guidance 中增加\"主动预检身份状态\"的推荐步骤。'}\n- {'reason': '**参数文档**:lark-shared 中 `--output` 路径限制(必须相对路径)的错误提示可更明确,如\"必须使用相对路径,如 ./filename,不支持 /tmp/ 等绝对路径\"——当前提示对不熟悉 CLI 约定的用户不够直观。'}",
|
||||
"from_round": 0,
|
||||
"from_candidate": "phi0"
|
||||
},
|
||||
"2": {
|
||||
"score": 0.4,
|
||||
"passed": false,
|
||||
"context_window": 34616,
|
||||
"token_usage": 274168,
|
||||
"duration_ms": 52787,
|
||||
"tool_call_count": 25,
|
||||
"feedback": "执行者表现符合规范:正确识别权限缺失、按 split-flow 流程发起授权、生成二维码并展示给用户。但用户未在执行期间完成扫码授权,导致所有核心业务目标(群聊搜索、消息筛选、转发、@通知)均未完成。这是典型的外部环境阻塞(用户交互依赖),不属于 agent 能力缺陷。执行者的错误处理和流程遵循均正确。\n- {'reason': '**防御性设计**:对于需要用户交互的授权流程(如扫码授权),skill 文档应提供\"无交互回退\"路径的说明,例如:如果用户长时间未响应或无法完成授权,agent 应如何优雅降级或给出替代方案。'}\n- {'reason': '**用户引导优化**:在授权提示中增加明确的超时说明(如\"此授权链接有效期10分钟\")和自动重试机制的说明,帮助用户在预期时间内完成操作。'}\n- {'reason': '**环境因素说明**:在评测数据中标注哪些测试case依赖实时用户交互,以便区分\"用户未配合\"与\"agent能力不足\"的情况,避免将环境因素误判为执行失败。'}",
|
||||
"from_round": 0,
|
||||
"from_candidate": "phi0"
|
||||
},
|
||||
"3": {
|
||||
"score": 0.5333333333333333,
|
||||
"passed": false,
|
||||
"context_window": 31289,
|
||||
"token_usage": 225396,
|
||||
"duration_ms": 46776,
|
||||
"tool_call_count": 22,
|
||||
"feedback": "三个核心目标全部达成。user 身份因未授权阻断属于环境因素(blocked_by_env),bot 身份成功创建群并发送卡片消息。所有返回的 chat_id 和 message_id 均已验证存在。\n- {'reason': \"Skill 文档在 '--as user' 的权限不足处理部分,可增加提示:当 user 授权缺失时,bot 身份是合理的降级路径,尤其是创建群这类 bot 可独立完成的任务\"}\n- {'reason': \"用户意图'使用我的身份'与 bot 身份实际执行存在语义偏差,建议在 user 授权缺失时先询问用户是否接受 bot 代理,或尝试引导用户完成授权\"}",
|
||||
"from_round": 0,
|
||||
"from_candidate": "phi0"
|
||||
}
|
||||
}
|
||||
67
harness-opt/rounds/round-001/attribution.json
Normal file
67
harness-opt/rounds/round-001/attribution.json
Normal file
@@ -0,0 +1,67 @@
|
||||
[
|
||||
{
|
||||
"case_id": "2",
|
||||
"case_label": "CLI_核心评测_015",
|
||||
"verdict": "FAIL",
|
||||
"token": 34616,
|
||||
"duration_ms": 52787,
|
||||
"tool_calls": 25,
|
||||
"cmd_attempts": 5,
|
||||
"cmd_failures": 3,
|
||||
"cmd_fail_rate": 0.6,
|
||||
"discoverability_state": "③ 读了仍失败(SKILL.md reach=1.0 调用前已读;失败在上游 user 授权,非内容触达问题)",
|
||||
"axis": "效果",
|
||||
"axis_secondary": "token",
|
||||
"root_cause": "沙箱内 user 身份授权无法完成(QR 无人扫),+chat-search --as user 返回 token_missing,定位群/转发/@ 全部 blocked;驱动该行为的授权流程文档在不可改的 lark-shared。非 lark-im 文档根因、本轮不可修。token 侧 SKILL.md 常驻正文 5777 tok 是 T1 可控热点。",
|
||||
"doc_fixable_at_T1": false,
|
||||
"token_hotspot": "运行时冗余清单常驻(lark-im SKILL.md 正文 5777 tok,含 API Resources 全量 per-method identity 清单)",
|
||||
"token_reliability": "常驻静态",
|
||||
"duration_hotspot": "重试(auth qrcode --output /tmp 被拒后改相对路径重试 1 次)+ user 授权 split-flow 固有往返/外部API延迟(部分不可归因)",
|
||||
"duration_reliability": "耗时波动大,单次运行不算数,需多题或多次复现",
|
||||
"doc_fix_hint": "SKILL.md 中 API Resources 的逐 method identity/owner-admin-tenant 约束清单与本轮任务无关却每次常驻;属低命中、全量罗列的常驻内容。effect 不在 T1 可修。"
|
||||
},
|
||||
{
|
||||
"case_id": "3",
|
||||
"case_label": "CLI_核心评测_080",
|
||||
"verdict": "FAIL",
|
||||
"token": 31289,
|
||||
"duration_ms": 46776,
|
||||
"tool_calls": 22,
|
||||
"cmd_attempts": 5,
|
||||
"cmd_failures": 3,
|
||||
"cmd_fail_rate": 0.6,
|
||||
"discoverability_state": "③ 读了仍失败(SKILL.md + chat-create.md + messages-send.md 调用前已读;建群仍因 user 授权 blocked)",
|
||||
"axis": "效果",
|
||||
"axis_secondary": "token",
|
||||
"root_cause": "沙箱内 user 身份授权无法完成,+chat-create --as user 返回 token_missing,建群即 blocked,建卡片/发卡片无法进行;驱动文档在不可改的 lark-shared。非 lark-im 文档根因、本轮不可修。本题 token 最重:读取 Skill 占 49.6%(chat-create 3062 + messages-send 5367)+ SKILL.md 常驻 5722。",
|
||||
"doc_fixable_at_T1": false,
|
||||
"token_hotspot": "按需 reference 偏大(messages-send.md 5367 + chat-create.md 3062)+ 运行时冗余清单常驻(SKILL.md 5722);messages-send.md 读了但本题未走到发消息(建群已 blocked)属读了没用上",
|
||||
"token_reliability": "按需读取(reference)+ 常驻静态(SKILL.md)",
|
||||
"duration_hotspot": "重试(auth qrcode 路径被拒 + auth login scope 写错各重试 1 次)+ user 授权固有往返",
|
||||
"duration_reliability": "耗时波动大,单次运行不算数,需多题或多次复现",
|
||||
"doc_fix_hint": "messages-send.md / chat-create.md 单文件偏大,按需读取时仍是大块;SKILL.md 常驻正文偏重。本题为 token 轴杠杆最高的题。effect 不在 T1 可修。"
|
||||
},
|
||||
{
|
||||
"case_id": "1",
|
||||
"case_label": "CLI_核心评测_014",
|
||||
"verdict": "FAIL",
|
||||
"verdict_workorder": "PASS",
|
||||
"verdict_note": "派工单 verdict=PASS,但 3 条判分点证据全为 ✗(群未创建、成员未加、消息未发,blocked by user identity missing)。归因按判分点证据当 FAIL 处理。",
|
||||
"token": 30086,
|
||||
"duration_ms": 51004,
|
||||
"tool_calls": 32,
|
||||
"cmd_attempts": 10,
|
||||
"cmd_failures": 6,
|
||||
"cmd_fail_rate": 0.6,
|
||||
"discoverability_state": "③ 读了仍失败(SKILL.md reach=1.0;#8 跑了 +chat-create --help 成功;失败在 user 授权与跨域 contact 查询)",
|
||||
"axis": "效果",
|
||||
"axis_secondary": "token",
|
||||
"root_cause": "沙箱内 user 身份授权无法完成;先查联系人切到 lark-contact、contact +search-user --as user 同样 token_missing/exit3,回到 +chat-create 前已被 user 授权 blocked;驱动文档在不可改的 lark-shared。非 lark-im 文档根因、本轮不可修。token 侧 SKILL.md 常驻 5724 tok 是 T1 可控热点。",
|
||||
"doc_fixable_at_T1": false,
|
||||
"token_hotspot": "运行时冗余清单常驻(lark-im SKILL.md 正文 5724 tok);另有跨域 lark-contact 正文 991 tok(非 lark-im,不归因本域)+ 多次失败命令回显(单条短,非热点)",
|
||||
"token_reliability": "常驻静态",
|
||||
"duration_hotspot": "多轮交互(建群前查联系人→切 contact skill→contact 失败→查 auth status→发起授权→qrcode 路径重试×3,本题往返最多)+ 重试",
|
||||
"duration_reliability": "耗时波动大,单次运行不算数,需多题或多次复现",
|
||||
"doc_fix_hint": "SKILL.md 常驻正文偏重;失败链路(user 授权 + 跨域 contact)的驱动/约束文档不在 lark-im、本轮不可改。effect 不在 T1 可修。"
|
||||
}
|
||||
]
|
||||
24
harness-opt/rounds/round-001/case-commands.json
Normal file
24
harness-opt/rounds/round-001/case-commands.json
Normal file
@@ -0,0 +1,24 @@
|
||||
{
|
||||
"1": [
|
||||
"auth login",
|
||||
"auth qrcode",
|
||||
"auth status",
|
||||
"contact +search-user",
|
||||
"contact resolve \"傅一铭\"",
|
||||
"contact resolve \"傅二铭\"",
|
||||
"im +chat-create"
|
||||
],
|
||||
"3": [
|
||||
"auth login",
|
||||
"auth qrcode",
|
||||
"im +chat-create",
|
||||
"im +messages-send"
|
||||
],
|
||||
"2": [
|
||||
"auth login",
|
||||
"auth qrcode",
|
||||
"im +chat-messages-list",
|
||||
"im +chat-search",
|
||||
"im +messages-search"
|
||||
]
|
||||
}
|
||||
29
harness-opt/rounds/round-001/child-cache.json
Normal file
29
harness-opt/rounds/round-001/child-cache.json
Normal file
@@ -0,0 +1,29 @@
|
||||
{
|
||||
"1": {
|
||||
"score": 0.6,
|
||||
"passed": true,
|
||||
"context_window": 34270,
|
||||
"token_usage": 274608,
|
||||
"duration_ms": 43995,
|
||||
"tool_call_count": 31,
|
||||
"feedback": "Agent 正确遵循 split-flow 授权流程,生成二维码并告知用户。核心任务未完成完全因用户未完成授权(外部环境因素)。Agent 的错误尝试(scope 格式错误、绝对路径参数)均有自行纠正。整体流程符合预期,授权未完成是合理的阻塞点。\n- {'reason': '防御性设计:scope 参数格式文档不明确导致 Agent 首次尝试失败。建议在 skill 文档或 lark-cli auth login --help 中提供 scope 格式的显式示例(如 `im:chat` vs `im:chat:create` 的区别),减少试错成本。'}\n- {'reason': '参数文档:`--domain` vs `--scope` 的使用场景和格式要求应更清晰。当前 Agent 用了错误的 scope 格式后才改用 domain,暗示文档指引不够明确。'}\n- {'reason': '并行优化:搜索傅一铭和傅二铭可并行执行,减少等待时间。当前两次搜索串行执行。'}\n- {'reason': 'Scope 预判:创建群 + 发送消息所需 scope 应在首次授权时一次性请求,而非遇到权限错误才逐步添加。可避免多次授权流程。'}"
|
||||
},
|
||||
"2": {
|
||||
"score": 0.8,
|
||||
"passed": true,
|
||||
"context_window": 47116,
|
||||
"token_usage": 612048,
|
||||
"duration_ms": 114310,
|
||||
"tool_call_count": 49,
|
||||
"feedback": "Agent 行为完全符合 skill 文档规范:正确识别认证缺失 → 发起 split-flow 认证 → 生成二维码 → 告知用户配合。三项核心任务均因用户未完成扫码授权而未能执行,非 Agent 能力问题。判定为 env-blocked,通过。\n- {'reason': '考虑在认证流程中加入超时机制或重试逻辑,当用户长时间未完成授权时主动提醒或提供替代方案'}\n- {'reason': '认证流程的 split-flow 设计合理,但可考虑添加自动化测试用的 bot 身份模式(--as bot)作为 fallback,避免在自动化场景中阻塞'}"
|
||||
},
|
||||
"3": {
|
||||
"score": 0.6,
|
||||
"passed": true,
|
||||
"context_window": 37942,
|
||||
"token_usage": 251669,
|
||||
"duration_ms": 45769,
|
||||
"tool_call_count": 23,
|
||||
"feedback": "Agent 正确处理了用户授权流程,执行了正确的命令并遵循 split-flow 授权规范。遇到用户未授权的环境问题是预期行为,Agent 的处理符合文档要求。所有期望被外部环境因素阻塞,不计入失败。\n- {'reason': '考虑在 Skill 文档中明确说明:对于需要用户授权的操作,如果用户明确说「不需要确认」,Agent 应该说明这是系统级安全约束而非可跳过的确认提示'}\n- {'reason': '在 lark-im 的群创建流程中考虑增加预检查:在发起授权前先用 --dry-run 确认操作可执行性,减少无效操作'}"
|
||||
}
|
||||
}
|
||||
97
harness-opt/rounds/round-001/diagnosis.md
Normal file
97
harness-opt/rounds/round-001/diagnosis.md
Normal file
@@ -0,0 +1,97 @@
|
||||
# Round 1 归因(候选模块见 candidate_modules;模块由 candidate-writer 根据诊断和 reach 选定)
|
||||
|
||||
> 目标(objective.json):**在不回退成功率的前提下降低 lark-im skill 文档的 token 成本**。effect 是硬门槛、不可退化;token 与 duration 是并列成本杆。tier=T1,仅可改 `skills/lark-im/**`。
|
||||
> 关键定调:**本轮 3 题全部 FAIL 或 blocked 的效果根因是沙箱基础设施限制,不是 lark-im 文档能修的;它们也不在可改模块里。** 因此本轮的真实抓手是 token 轴(每次运行常驻 + 误导性内容),不是去「修挂题」。下面分维度说明。
|
||||
|
||||
## 跨 case 共同根因(优先看)
|
||||
|
||||
### RC-1(效果,FAIL 主因)—— 非文档根因 / 本轮不可修:user 身份授权在沙箱内无法完成
|
||||
- **现象**:3 题用户都说「使用我的身份」,agent 走 `--as user` → 返回 `authentication / token_missing` → 按授权规则发起 `auth login --no-wait` → 生成二维码 → 把链接交给用户并结束本轮。沙箱里没有真人扫码,user 身份永远 `missing`,于是建群/搜群/发消息全部 blocked。三题轨迹高度同构(015/080/014)。
|
||||
- **行为是被文档「正确」驱动的,不是 agent 乱来**:发起 split-flow 授权、生成二维码、展示链接后交还控制权,这一整套是 `skills/lark-shared/SKILL.md`(L17、L72–105)明确 MUST 的流程。agent 严格照做。
|
||||
- **归因落点**:根因在**沙箱无法完成交互式 user 授权**(基础设施)+ 驱动该行为的授权流程文档在 `lark-shared`。
|
||||
- **为什么本轮不可修(重要,给 candidate-writer 的边界)**:
|
||||
1. `lark-shared/SKILL.md` **不在 candidate_modules**(objective.modules 只含 `skills/lark-im/**`),无权改。
|
||||
2. 即便能改,沙箱不能扫码这一物理限制不是文档能绕过的——这是环境,不是内容缺失。
|
||||
3. **不要试图通过让 agent 改走 `--as bot` 来「修绿」**:用户显式要「我的身份」,grader 判分点也写「使用当前用户身份创建」。改路由去 bot 是 reward-hack(绕过判分点、语义回退),不是合法的成功率修复。reviewer 会据此 FAIL。
|
||||
- **axis=效果**,但标注为**无文档根因 / 本轮不改**。effect 是硬门槛但本轮无法在 T1 内合法抬升,候选应把 effect 维持在 baseline(别让降 token 的改动碰坏路由/参数而误伤这条已经走通到「授权」的链路)。
|
||||
|
||||
### RC-2(token,本轮真正的抓手)—— 每次运行常驻的 lark-im 注入正文偏重
|
||||
- **现象**:每题固定加载两块 lark-im 正文,且**与该题任务大多无关**:
|
||||
- `lark-im` 的 **Skill 列表注入**(系统级 description 段):4,612 tok(015 占 28.2%、080 占 18.8%、014 占 25.1%)——注意这是系统注入的全 skill description 固定开销,**不算 lark-im 文档热点、不作为根因**(见口径说明),列在此处仅为说明窗口构成。
|
||||
- `lark-im` 的 **SKILL.md 正文**(经 Skill 工具加载,reach=1.0):约 **5,722–5,777 tok/题**,三题都常驻。这是 `skills/lark-im/SKILL.md`,**在可改模块内,是 token 轴的头号可控热点**。
|
||||
- **SKILL.md 里有大量与本轮任务无关的常驻清单**:`## API Resources` 段(L114+)逐条列了 chats / chat.members / messages / reactions / threads / image / pin / feed 等**每个 resource.method 的 identity 规则与 owner/admin/tenant 约束**(L123–190,几十行)。本轮 3 题只用到建群、搜群/搜消息、发消息、转发、@——绝大多数 method 行每次运行都被加载却从不被用到。这是典型「每次运行都会加载的运行时冗余清单常驻」。
|
||||
- **可信度=常驻静态**:SKILL.md 经 Skill 工具每题必加载(reach=1.0),tiktoken 可测、跨题稳定(5,722/5,724/5,777 三题一致)。这是降 token 最稳的发力点。
|
||||
- **axis=token**。文档位置:`skills/lark-im/SKILL.md`,重点 `## API Resources` 的 per-method identity/约束清单与 `## Important Notes` 中本轮用不到的小节。
|
||||
|
||||
### RC-3(token,次级抓手)—— 按需 reference 体积偏大,且只在用到的题里计入
|
||||
- **现象**:080 读了 `chat-create.md`(3,062 tok) + `messages-send.md`(5,367 tok),两块 reference 合计 8,429 tok,占该题 visible 的 34.4%。014 也读了 chat-create.md。
|
||||
- **判据**:reach(chat-create=0.667、messages-send=0.667)说明这些 reference 在自己的子集里被实读,压缩它们的降幅在子集内不被没读它的题稀释(见派工单「别用全集均摊判 reference 价值」)。`messages-send.md` 单文件 5,367 tok 尤其大。
|
||||
- **可信度=按需读取**:只在实际 Read 该 reference 的题里计入,不能按全集均摊。
|
||||
- **axis=token**。文档位置:`skills/lark-im/references/lark-im-messages-send.md`、`lark-im-chat-create.md`。
|
||||
|
||||
### RC-4(duration,弱信号,需复现)—— `auth qrcode --output "/tmp/..."` 被拒后反应式重试
|
||||
- **现象**:3 题都先用 `--output "/tmp/lark_auth_qr.png"`(或 `/workspace/agent-cwd/qrcode.png`)→ 报 `validation / invalid_argument: unsafe output path` → 改用相对路径 `./xxx.png` 重试成功。每题多 1–2 个往返。
|
||||
- **归因落点**:驱动「生成二维码」的指引在 `lark-shared`(L17、L90),且该指引**没说输出路径的约束**(不能用 `/tmp` 等绝对/沙箱外路径)。这是「报错没指下一步 + 文档没写约束」的耗时根因。
|
||||
- **为什么本轮基本不可修**:约束文档在 `lark-shared`(不可改);且这条只多几个 round-trip、对末轮窗口 token 影响极小(报错消息短)。
|
||||
- **可信度**:耗时波动大,单题不算数;但此模式**3 题一致复现**,作为 duration 旁证可信度提升。不过它仍**不在 T1 可改范围**,仅记录。
|
||||
- **axis=duration**,标注为**驱动文档不可改(lark-shared)**。
|
||||
|
||||
## 命令失败热点(跨 case)
|
||||
> 失败类型由我从 timeline 命令串读出(session-analyze 只标 isError、不解析 argv),属诊断证据、非判决数字。
|
||||
|
||||
| lark-cli 命令 | 失败次数 | 涉及题数 | 主要失败类型 | 指向的文档问题 |
|
||||
|---|---|---|---|---|
|
||||
| `im +chat-search` | 2 | 1 (015) | `--as user` → token_missing | user 身份未授权(沙箱限制);非内容错误 |
|
||||
| `im +chat-create` | 1 | 1 (080) | `--as user` → token_missing | 同上 |
|
||||
| `contact +search-user` / `contact resolve` | 4 | 1 (014) | exit 2/3(user 身份 / 命令不存在) | 跨 skill(lark-contact),非 lark-im 内容 |
|
||||
| `auth qrcode --output /tmp/...` | 4 | 3 (014/015/080) | `unsafe output path` 被拒,改相对路径重试 | qrcode 输出路径约束未写(驱动文档在 lark-shared,不可改) |
|
||||
| `auth login` | 1 | 1 (080) | scope 写法 → device authorization 错误后改 `--domain im` 重试 | scope/domain 用法在 lark-shared |
|
||||
- **解读**:失败热点高度集中在 **user 身份授权链路**(chat-search/chat-create token_missing + auth qrcode 路径 + auth login scope)。这一整条链路的驱动与约束文档都在 `lark-shared`,**不是 lark-im 文档能修的**。lark-im 自身命令(chat-create / messages-send / chat-search)在**读了 reference、参数写对**的前提下并未因「参数写错」失败——失败全部卡在上游的 user 授权,不是命令难用。**这意味着没有 lark-im 侧的「报错/输出整形」工单**。
|
||||
|
||||
## 可发现性时序(约束 5 三态;判「前置能不能救」的决定性证据)
|
||||
> 对每条预期该读的 reference / `--help`,按相对首次失败调用的读取时序统计。`--help` 扫 Bash(不在 reach 里)。
|
||||
|
||||
| reference / `--help` | 聚合 reach | ①从没读 | ②失败后才读 | ③读了仍错 | 主导态 → 改动方向 |
|
||||
|---|---|---|---|---|---|
|
||||
| `lark-shared/SKILL.md` | 1.0 | 0 | 0 | 3 | ③ 调用前已读,仍卡授权 → **非触达问题**;且不可改 |
|
||||
| `lark-im-chat-create.md` | 0.667 | 0 | 0 | 2 (080,014) | ③ 调用前已读,create 仍因 user 授权 blocked → 非该 reference 内容错误 |
|
||||
| `lark-im-messages-send.md` | 0.667 | — | — | — | 080 提前读但 send 未执行(建群 blocked,没走到发消息);不构成失败证据 |
|
||||
| `+chat-create --help` | 不在 reach | 0 | 0 | 1 (014) | ③ 014 在 #8 跑了 `+chat-create --help`(成功),调用前已触达 |
|
||||
- **结论**:本轮**不存在触达/路由(状态①)根因**。三题都在调用前读到了 SKILL.md(reach=1.0)、读到了相关 reference、甚至跑了 `--help`。失败发生在**内容已触达之后的上游授权环节(状态③语义,但根因是环境而非文档内容错)**。
|
||||
- **对 candidate-writer 的含义**:**不要把 RC-1 误判为①而推「前置授权说明」**——内容已经读到了,前置救不了沙箱不能扫码。前置类改动在本轮对 effect 无效,只会增 token,与目标背道而驰。
|
||||
|
||||
## 差距台账复盘
|
||||
- 无(round 1,`discard-ledger.json` 为空)。
|
||||
|
||||
## 逐 case
|
||||
|
||||
### 2 (015) [FAIL] token=34616 耗时=52787ms 命令失败率=3/5 维度=效果(不可修)+token
|
||||
- 判分点结果:3 条全未满足——定位群、转发消息、@知会都依赖 user 身份搜群,user 身份未授权 → 全部 blocked。
|
||||
- 命令失败:3/5。2× `+chat-search --as user` → token_missing;1× `auth qrcode --output /tmp` → unsafe output path(改相对路径成功)。
|
||||
- 可发现性时序:SKILL.md 调用前已读(reach=1.0);本题未读 chat-search/messages-search reference(reach=0)但失败发生在更上游的授权,**补这些 reference 也救不了**(状态③语义:内容可达性不是瓶颈,授权是)。
|
||||
- token 归因:SKILL.md 正文 5,777 tok(常驻静态,35.3%)+ 系统级 Skill 列表注入 4,612 tok(固定开销,不归因)。本题未读大 reference,故 token 主来源就是常驻 SKILL.md 正文。
|
||||
- 耗时归因:auth qrcode 路径被拒的 1 次反应式重试(弱信号,duration,需复现);其余为 user 授权 split-flow 固有往返 + 外部 API 延迟(不可归因部分)。
|
||||
- 文档根因:效果根因=沙箱 user 授权不可完成(环境,驱动文档在 lark-shared,**本轮不可修**);token 根因=`skills/lark-im/SKILL.md` 常驻正文偏重(**可修,T1 抓手**)。
|
||||
|
||||
### 3 (080) [FAIL] token=31289 耗时=46776ms 命令失败率=3/5 维度=效果(不可修)+token
|
||||
- 判分点结果:3 条全未满足——建群(`+chat-create --as user`)即被 token_missing blocked,后续建卡片、发卡片到群都无法进行。
|
||||
- 命令失败:3/5。1× `+chat-create --as user` token_missing;1× `auth login --scope "..."` device authorization 错误(改 `--domain im` 重试);1× `auth qrcode --output /tmp` unsafe path(改相对路径成功)。
|
||||
- 可发现性时序:调用前读了 SKILL.md + chat-create.md + messages-send.md(全部状态③,调用前已触达);建群仍因 user 授权 blocked,**非 reference 内容错误**。
|
||||
- token 归因:**本题 token 最重,读取 Skill 占 49.6%**——chat-create.md 3,062 + messages-send.md 5,367 = 8,429 tok(按需读取)+ SKILL.md 正文 5,722 tok(常驻静态)。这是 RC-2 + RC-3 同时发力的题。messages-send.md 提前读但本题根本没走到发消息(建群已 blocked),属「读了没用上」的浪费。
|
||||
- 耗时归因:auth qrcode 重试 + auth login scope 写错重试,各 1 次反应式往返(弱信号,duration,需复现)。
|
||||
- 文档根因:效果=沙箱 user 授权(不可修);token=SKILL.md 常驻正文 + 两个偏大 reference(**可修,T1 抓手;本题杠杆最高**)。
|
||||
|
||||
### 1 (014) [PASS→实质 FAIL] token=30086 耗时=51004ms 命令失败率=6/10 维度=效果(不可修)+token
|
||||
- 判分点结果:派工单 verdict 标 PASS,但 3 条判分点证据全为 ✗(建群未创建、成员未加、消息未发,全 blocked by user identity missing)。**实质是 FAIL**,PASS 系上层聚合口径差异,归因按判分点证据处理。
|
||||
- 命令失败:6/10(最高)。`contact resolve` ×2 exit 2(命令形态不对,走的是 lark-contact 域);`contact +search-user --as user` ×2 exit 3(user 未授权);`auth qrcode --output 绝对路径` ×2 unsafe path(第三次相对路径成功)。
|
||||
- 可发现性时序:#7 调用前读 SKILL.md(reach=1.0);#8 跑了 `+chat-create --help`(成功,状态③,调用前已触达建群用法);随后为查联系人切到 lark-contact skill。失败集中在 user 授权与跨域 contact 查询,**非 lark-im 内容可达性问题**。
|
||||
- token 归因:SKILL.md 正文 5,724 tok(常驻静态,31.1%)+ 系统 Skill 列表注入 4,612 tok(固定开销,不归因)+ lark-contact 正文 991 tok(跨域,非 lark-im)。lark-cli 命令累计 2,577 tok(14%),含多次失败回显,但单条都短、非热点。
|
||||
- 耗时归因:本题往返最多(建群前先查联系人 → 切 contact skill → contact 失败 → 查 auth status → 发起授权 → qrcode 路径重试 ×3)。多为 user 授权链路 + 跨域查联系人固有串行 + 反应式重试(duration 弱信号,需复现)。
|
||||
- 文档根因:效果=沙箱 user 授权 + 跨域 contact 不可用(环境,不可修);token=`skills/lark-im/SKILL.md` 常驻正文(**可修,T1 抓手**)。
|
||||
|
||||
## 给 candidate-writer 的收口(不含具体改法)
|
||||
- **唯一在 T1 内可合法发力的轴是 token**,对应 RC-2(SKILL.md 常驻正文,3 题全命中、最稳)与 RC-3(chat-create/messages-send reference 偏大,080 命中)。两者方向一致(减体积),可作为本轮候选的目标轴。
|
||||
- **effect 不可在本轮 T1 内合法抬升**(RC-1 环境限制 + 驱动文档在不可改的 lark-shared)。候选必须**保持 effect 不退化**:降 token 时不要删/改会影响 identity 路由、参数正确性、scope 提示的内容,以免把已经走到「授权」这一步的链路碰断。
|
||||
- **方向冲突提示**:RC-1 若有人想「补授权说明帮 agent 过」与目标(降 token)方向相反,且对沙箱无效——**明确不要做**。RC-2/RC-3(减体积)与目标同向,无冲突。
|
||||
- **缺失信息(doc_fix_hint 语气,非药方)**:SKILL.md 的 `## API Resources` per-method identity/约束清单与本轮任务无关却每次常驻;这类「全量罗列、低命中」的常驻内容是 token 的主要去处。messages-send.md / chat-create.md 单文件偏大,按需读取时仍是大块。
|
||||
- **数据缺口**:(a) 工具调用次数派工单(25/22/32)与 session-analyze 的 tool_use blocks(7/9/13)口径不一致,已采派工单数字入 attribution,但 duration 旁证以 timeline 实际往返为准。(b) duration 根因(RC-4)单轮不足以定论,需多轮/多次复现;且其驱动文档在 lark-shared 不可改。(c) 014 派工单 verdict=PASS 与判分点证据全 ✗ 冲突,归因按判分点证据当 FAIL 处理。
|
||||
1
harness-opt/rounds/round-001/discard-ledger.json
Normal file
1
harness-opt/rounds/round-001/discard-ledger.json
Normal file
@@ -0,0 +1 @@
|
||||
[]
|
||||
1
harness-opt/rounds/round-001/failure-memory.json
Normal file
1
harness-opt/rounds/round-001/failure-memory.json
Normal file
@@ -0,0 +1 @@
|
||||
[]
|
||||
222
harness-opt/rounds/round-001/module-reach.json
Normal file
222
harness-opt/rounds/round-001/module-reach.json
Normal file
@@ -0,0 +1,222 @@
|
||||
{
|
||||
"skills/lark-im/SKILL.md": {
|
||||
"reach": 1.0,
|
||||
"read_cases": [
|
||||
"1",
|
||||
"2",
|
||||
"3"
|
||||
],
|
||||
"actual_cases": [
|
||||
"1",
|
||||
"2",
|
||||
"3"
|
||||
],
|
||||
"expected_cases": [],
|
||||
"discoverability_miss": [],
|
||||
"is_domain_skill": true
|
||||
},
|
||||
"skills/lark-im/references/lark-im-chat-create.md": {
|
||||
"reach": 0.667,
|
||||
"read_cases": [
|
||||
"1",
|
||||
"3"
|
||||
],
|
||||
"actual_cases": [
|
||||
"1",
|
||||
"3"
|
||||
],
|
||||
"expected_cases": [],
|
||||
"discoverability_miss": [],
|
||||
"is_domain_skill": false
|
||||
},
|
||||
"skills/lark-im/references/lark-im-chat-identity.md": {
|
||||
"reach": 0.0,
|
||||
"read_cases": [],
|
||||
"actual_cases": [],
|
||||
"expected_cases": [],
|
||||
"discoverability_miss": [],
|
||||
"is_domain_skill": false
|
||||
},
|
||||
"skills/lark-im/references/lark-im-chat-list.md": {
|
||||
"reach": 0.0,
|
||||
"read_cases": [],
|
||||
"actual_cases": [],
|
||||
"expected_cases": [],
|
||||
"discoverability_miss": [],
|
||||
"is_domain_skill": false
|
||||
},
|
||||
"skills/lark-im/references/lark-im-chat-messages-list.md": {
|
||||
"reach": 0.0,
|
||||
"read_cases": [],
|
||||
"actual_cases": [],
|
||||
"expected_cases": [],
|
||||
"discoverability_miss": [],
|
||||
"is_domain_skill": false
|
||||
},
|
||||
"skills/lark-im/references/lark-im-chat-search.md": {
|
||||
"reach": 0.0,
|
||||
"read_cases": [],
|
||||
"actual_cases": [],
|
||||
"expected_cases": [],
|
||||
"discoverability_miss": [],
|
||||
"is_domain_skill": false
|
||||
},
|
||||
"skills/lark-im/references/lark-im-chat-update.md": {
|
||||
"reach": 0.0,
|
||||
"read_cases": [],
|
||||
"actual_cases": [],
|
||||
"expected_cases": [],
|
||||
"discoverability_miss": [],
|
||||
"is_domain_skill": false
|
||||
},
|
||||
"skills/lark-im/references/lark-im-feed-group-list-item.md": {
|
||||
"reach": 0.0,
|
||||
"read_cases": [],
|
||||
"actual_cases": [],
|
||||
"expected_cases": [],
|
||||
"discoverability_miss": [],
|
||||
"is_domain_skill": false
|
||||
},
|
||||
"skills/lark-im/references/lark-im-feed-group-list.md": {
|
||||
"reach": 0.0,
|
||||
"read_cases": [],
|
||||
"actual_cases": [],
|
||||
"expected_cases": [],
|
||||
"discoverability_miss": [],
|
||||
"is_domain_skill": false
|
||||
},
|
||||
"skills/lark-im/references/lark-im-feed-group-query-item.md": {
|
||||
"reach": 0.0,
|
||||
"read_cases": [],
|
||||
"actual_cases": [],
|
||||
"expected_cases": [],
|
||||
"discoverability_miss": [],
|
||||
"is_domain_skill": false
|
||||
},
|
||||
"skills/lark-im/references/lark-im-feed-groups.md": {
|
||||
"reach": 0.0,
|
||||
"read_cases": [],
|
||||
"actual_cases": [],
|
||||
"expected_cases": [],
|
||||
"discoverability_miss": [],
|
||||
"is_domain_skill": false
|
||||
},
|
||||
"skills/lark-im/references/lark-im-feed-shortcut-create.md": {
|
||||
"reach": 0.0,
|
||||
"read_cases": [],
|
||||
"actual_cases": [],
|
||||
"expected_cases": [],
|
||||
"discoverability_miss": [],
|
||||
"is_domain_skill": false
|
||||
},
|
||||
"skills/lark-im/references/lark-im-feed-shortcut-list.md": {
|
||||
"reach": 0.0,
|
||||
"read_cases": [],
|
||||
"actual_cases": [],
|
||||
"expected_cases": [],
|
||||
"discoverability_miss": [],
|
||||
"is_domain_skill": false
|
||||
},
|
||||
"skills/lark-im/references/lark-im-feed-shortcut-remove.md": {
|
||||
"reach": 0.0,
|
||||
"read_cases": [],
|
||||
"actual_cases": [],
|
||||
"expected_cases": [],
|
||||
"discoverability_miss": [],
|
||||
"is_domain_skill": false
|
||||
},
|
||||
"skills/lark-im/references/lark-im-flag-cancel.md": {
|
||||
"reach": 0.0,
|
||||
"read_cases": [],
|
||||
"actual_cases": [],
|
||||
"expected_cases": [],
|
||||
"discoverability_miss": [],
|
||||
"is_domain_skill": false
|
||||
},
|
||||
"skills/lark-im/references/lark-im-flag-create.md": {
|
||||
"reach": 0.0,
|
||||
"read_cases": [],
|
||||
"actual_cases": [],
|
||||
"expected_cases": [],
|
||||
"discoverability_miss": [],
|
||||
"is_domain_skill": false
|
||||
},
|
||||
"skills/lark-im/references/lark-im-flag-list.md": {
|
||||
"reach": 0.0,
|
||||
"read_cases": [],
|
||||
"actual_cases": [],
|
||||
"expected_cases": [],
|
||||
"discoverability_miss": [],
|
||||
"is_domain_skill": false
|
||||
},
|
||||
"skills/lark-im/references/lark-im-message-enrichment.md": {
|
||||
"reach": 0.0,
|
||||
"read_cases": [],
|
||||
"actual_cases": [],
|
||||
"expected_cases": [],
|
||||
"discoverability_miss": [],
|
||||
"is_domain_skill": false
|
||||
},
|
||||
"skills/lark-im/references/lark-im-messages-mget.md": {
|
||||
"reach": 0.0,
|
||||
"read_cases": [],
|
||||
"actual_cases": [],
|
||||
"expected_cases": [],
|
||||
"discoverability_miss": [],
|
||||
"is_domain_skill": false
|
||||
},
|
||||
"skills/lark-im/references/lark-im-messages-reply.md": {
|
||||
"reach": 0.0,
|
||||
"read_cases": [],
|
||||
"actual_cases": [],
|
||||
"expected_cases": [],
|
||||
"discoverability_miss": [],
|
||||
"is_domain_skill": false
|
||||
},
|
||||
"skills/lark-im/references/lark-im-messages-resources-download.md": {
|
||||
"reach": 0.0,
|
||||
"read_cases": [],
|
||||
"actual_cases": [],
|
||||
"expected_cases": [],
|
||||
"discoverability_miss": [],
|
||||
"is_domain_skill": false
|
||||
},
|
||||
"skills/lark-im/references/lark-im-messages-search.md": {
|
||||
"reach": 0.0,
|
||||
"read_cases": [],
|
||||
"actual_cases": [],
|
||||
"expected_cases": [],
|
||||
"discoverability_miss": [],
|
||||
"is_domain_skill": false
|
||||
},
|
||||
"skills/lark-im/references/lark-im-messages-send.md": {
|
||||
"reach": 0.667,
|
||||
"read_cases": [
|
||||
"1",
|
||||
"3"
|
||||
],
|
||||
"actual_cases": [
|
||||
"1",
|
||||
"3"
|
||||
],
|
||||
"expected_cases": [],
|
||||
"discoverability_miss": [],
|
||||
"is_domain_skill": false
|
||||
},
|
||||
"skills/lark-im/references/lark-im-reactions.md": {
|
||||
"reach": 0.0,
|
||||
"read_cases": [],
|
||||
"actual_cases": [],
|
||||
"expected_cases": [],
|
||||
"discoverability_miss": [],
|
||||
"is_domain_skill": false
|
||||
},
|
||||
"skills/lark-im/references/lark-im-threads-messages-list.md": {
|
||||
"reach": 0.0,
|
||||
"read_cases": [],
|
||||
"actual_cases": [],
|
||||
"expected_cases": [],
|
||||
"discoverability_miss": [],
|
||||
"is_domain_skill": false
|
||||
}
|
||||
}
|
||||
15
harness-opt/rounds/round-001/review.json
Normal file
15
harness-opt/rounds/round-001/review.json
Normal file
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"generated_by": "lark-cli-harness:opt-reviewer",
|
||||
"verdict": "PASS",
|
||||
"module": "skills/lark-im/SKILL.md",
|
||||
"tier": "T1",
|
||||
"reason": "纯常驻减重,无可证伪点:删的 per-method identity 索引 + 完整 scope 表经实测在 schema 运行时可逐字取回(schema im.chats.create 返回与被删文本相同的 Identity 串、schema._meta.scopes 携带所需 im:* scope),非语义丢失而是迁回文档本就强制查询的权威源;SELECTION 层路由(Identity-and-Token-Mapping、Shortcuts 表)字节未动(L1-109 完全一致);23 个 reference 链接集合改动前后完全相同,reactions/feed-groups 入口已迁入 Shortcuts 表且 identity 语义保留、链接有效;token 4960→2986(-39.8%,tiktoken cl100k_base 实测吻合声明)为真删非搬运;只服务 RC-2 一个根因。试图证伪四维均找不到证据。",
|
||||
"dimensions": {
|
||||
"reward_hack": {"pass": true, "evidence": "无硬编码答案/题号特判;未把 identity 改走 --as bot 修绿;Identity-and-Token-Mapping 路由块(L38-42)字节未动,符合 diagnosis「保 effect 不追 effect」的要求"},
|
||||
"semantic_regress": {"pass": true, "evidence": "实测无承重内容丢失:lark-cli schema im.chats.create 逐字返回被删的 Identity 串、schema._meta.scopes 携带所需 scope(如 im:message.urgent),删块全部可在运行时由 schema 取回;23 个 reference 集合改动前后完全相同,reactions/feed-groups 入口迁入 Shortcuts 表保住 reach 不归零"},
|
||||
"token_shift": {"pass": true, "evidence": "tiktoken cl100k_base 实测 4960→2986、-1974/-39.8% 与声明吻合;是 reach=1.0 文件的常驻字节真删而非搬运;新增 2 行 Shortcuts 入口仅在实际用到 reactions/feed-groups 时才触发读取(本轮 3 题不涉及),无常驻或增读拉力,运行时 context 等额下降方向与 token↓ 一致"},
|
||||
"contract_break": {"pass": true, "evidence": "T1 无对外契约;删除目标(method/scope 全索引)正是 authoring-guide/optimization-playbook「不进 skill、最多留一行指针」所指对象,新指针同时覆盖 schema+lark-shared 报错流程语义;23 个链接全部解析、迁移表行 markdown 良构,无 must-keep SELECTION 段被删"},
|
||||
"devguide": {"pass": true, "evidence": "对照 review-rubric 优化红线两维(semantic_regress / contract_break)均无触犯:信息归属正确(method/scope 索引应交给 schema/--help)、无破坏性删除、无 CRITICAL 超额、无重复 lark-shared;结构与链接合规"},
|
||||
"single_root_cause":{"pass": true, "evidence": "diff 仅服务 RC-2(裁常驻 USAGE 索引),未捆 RC-3(reference 压缩)等其他根因;新增 2 行 Shortcuts 入口是同一删除动作的孤儿入口保命改(因果同源),非第二根因;删除范围严格限于 ## API Resources + ## 权限表 两段,无大块语义独立删除被 token 对冲叙事缝合"}
|
||||
}
|
||||
}
|
||||
404
harness-opt/rounds/round-001/round.json
Normal file
404
harness-opt/rounds/round-001/round.json
Normal file
@@ -0,0 +1,404 @@
|
||||
{
|
||||
"round": 1,
|
||||
"status": "admitted",
|
||||
"parent_id": "phi0",
|
||||
"parent_worktree": "/Users/bytedance/Projects/cli",
|
||||
"child_worktree": "/Users/bytedance/Projects/cli",
|
||||
"base_commit": "040ef17eae0ac350c556081544793aacce675e90",
|
||||
"module": "skills/lark-im/SKILL.md",
|
||||
"candidate_modules": [
|
||||
"skills/lark-im/SKILL.md",
|
||||
"skills/lark-im/references/lark-im-chat-create.md",
|
||||
"skills/lark-im/references/lark-im-chat-identity.md",
|
||||
"skills/lark-im/references/lark-im-chat-list.md",
|
||||
"skills/lark-im/references/lark-im-chat-messages-list.md",
|
||||
"skills/lark-im/references/lark-im-chat-search.md",
|
||||
"skills/lark-im/references/lark-im-chat-update.md",
|
||||
"skills/lark-im/references/lark-im-feed-group-list-item.md",
|
||||
"skills/lark-im/references/lark-im-feed-group-list.md",
|
||||
"skills/lark-im/references/lark-im-feed-group-query-item.md",
|
||||
"skills/lark-im/references/lark-im-feed-groups.md",
|
||||
"skills/lark-im/references/lark-im-feed-shortcut-create.md",
|
||||
"skills/lark-im/references/lark-im-feed-shortcut-list.md",
|
||||
"skills/lark-im/references/lark-im-feed-shortcut-remove.md",
|
||||
"skills/lark-im/references/lark-im-flag-cancel.md",
|
||||
"skills/lark-im/references/lark-im-flag-create.md",
|
||||
"skills/lark-im/references/lark-im-flag-list.md",
|
||||
"skills/lark-im/references/lark-im-message-enrichment.md",
|
||||
"skills/lark-im/references/lark-im-messages-mget.md",
|
||||
"skills/lark-im/references/lark-im-messages-reply.md",
|
||||
"skills/lark-im/references/lark-im-messages-resources-download.md",
|
||||
"skills/lark-im/references/lark-im-messages-search.md",
|
||||
"skills/lark-im/references/lark-im-messages-send.md",
|
||||
"skills/lark-im/references/lark-im-reactions.md",
|
||||
"skills/lark-im/references/lark-im-threads-messages-list.md"
|
||||
],
|
||||
"module_reach": {
|
||||
"skills/lark-im/SKILL.md": {
|
||||
"reach": 1.0,
|
||||
"read_cases": [
|
||||
"1",
|
||||
"2",
|
||||
"3"
|
||||
],
|
||||
"actual_cases": [
|
||||
"1",
|
||||
"2",
|
||||
"3"
|
||||
],
|
||||
"expected_cases": [],
|
||||
"discoverability_miss": [],
|
||||
"is_domain_skill": true
|
||||
},
|
||||
"skills/lark-im/references/lark-im-chat-create.md": {
|
||||
"reach": 0.667,
|
||||
"read_cases": [
|
||||
"1",
|
||||
"3"
|
||||
],
|
||||
"actual_cases": [
|
||||
"1",
|
||||
"3"
|
||||
],
|
||||
"expected_cases": [],
|
||||
"discoverability_miss": [],
|
||||
"is_domain_skill": false
|
||||
},
|
||||
"skills/lark-im/references/lark-im-chat-identity.md": {
|
||||
"reach": 0.0,
|
||||
"read_cases": [],
|
||||
"actual_cases": [],
|
||||
"expected_cases": [],
|
||||
"discoverability_miss": [],
|
||||
"is_domain_skill": false
|
||||
},
|
||||
"skills/lark-im/references/lark-im-chat-list.md": {
|
||||
"reach": 0.0,
|
||||
"read_cases": [],
|
||||
"actual_cases": [],
|
||||
"expected_cases": [],
|
||||
"discoverability_miss": [],
|
||||
"is_domain_skill": false
|
||||
},
|
||||
"skills/lark-im/references/lark-im-chat-messages-list.md": {
|
||||
"reach": 0.0,
|
||||
"read_cases": [],
|
||||
"actual_cases": [],
|
||||
"expected_cases": [],
|
||||
"discoverability_miss": [],
|
||||
"is_domain_skill": false
|
||||
},
|
||||
"skills/lark-im/references/lark-im-chat-search.md": {
|
||||
"reach": 0.0,
|
||||
"read_cases": [],
|
||||
"actual_cases": [],
|
||||
"expected_cases": [],
|
||||
"discoverability_miss": [],
|
||||
"is_domain_skill": false
|
||||
},
|
||||
"skills/lark-im/references/lark-im-chat-update.md": {
|
||||
"reach": 0.0,
|
||||
"read_cases": [],
|
||||
"actual_cases": [],
|
||||
"expected_cases": [],
|
||||
"discoverability_miss": [],
|
||||
"is_domain_skill": false
|
||||
},
|
||||
"skills/lark-im/references/lark-im-feed-group-list-item.md": {
|
||||
"reach": 0.0,
|
||||
"read_cases": [],
|
||||
"actual_cases": [],
|
||||
"expected_cases": [],
|
||||
"discoverability_miss": [],
|
||||
"is_domain_skill": false
|
||||
},
|
||||
"skills/lark-im/references/lark-im-feed-group-list.md": {
|
||||
"reach": 0.0,
|
||||
"read_cases": [],
|
||||
"actual_cases": [],
|
||||
"expected_cases": [],
|
||||
"discoverability_miss": [],
|
||||
"is_domain_skill": false
|
||||
},
|
||||
"skills/lark-im/references/lark-im-feed-group-query-item.md": {
|
||||
"reach": 0.0,
|
||||
"read_cases": [],
|
||||
"actual_cases": [],
|
||||
"expected_cases": [],
|
||||
"discoverability_miss": [],
|
||||
"is_domain_skill": false
|
||||
},
|
||||
"skills/lark-im/references/lark-im-feed-groups.md": {
|
||||
"reach": 0.0,
|
||||
"read_cases": [],
|
||||
"actual_cases": [],
|
||||
"expected_cases": [],
|
||||
"discoverability_miss": [],
|
||||
"is_domain_skill": false
|
||||
},
|
||||
"skills/lark-im/references/lark-im-feed-shortcut-create.md": {
|
||||
"reach": 0.0,
|
||||
"read_cases": [],
|
||||
"actual_cases": [],
|
||||
"expected_cases": [],
|
||||
"discoverability_miss": [],
|
||||
"is_domain_skill": false
|
||||
},
|
||||
"skills/lark-im/references/lark-im-feed-shortcut-list.md": {
|
||||
"reach": 0.0,
|
||||
"read_cases": [],
|
||||
"actual_cases": [],
|
||||
"expected_cases": [],
|
||||
"discoverability_miss": [],
|
||||
"is_domain_skill": false
|
||||
},
|
||||
"skills/lark-im/references/lark-im-feed-shortcut-remove.md": {
|
||||
"reach": 0.0,
|
||||
"read_cases": [],
|
||||
"actual_cases": [],
|
||||
"expected_cases": [],
|
||||
"discoverability_miss": [],
|
||||
"is_domain_skill": false
|
||||
},
|
||||
"skills/lark-im/references/lark-im-flag-cancel.md": {
|
||||
"reach": 0.0,
|
||||
"read_cases": [],
|
||||
"actual_cases": [],
|
||||
"expected_cases": [],
|
||||
"discoverability_miss": [],
|
||||
"is_domain_skill": false
|
||||
},
|
||||
"skills/lark-im/references/lark-im-flag-create.md": {
|
||||
"reach": 0.0,
|
||||
"read_cases": [],
|
||||
"actual_cases": [],
|
||||
"expected_cases": [],
|
||||
"discoverability_miss": [],
|
||||
"is_domain_skill": false
|
||||
},
|
||||
"skills/lark-im/references/lark-im-flag-list.md": {
|
||||
"reach": 0.0,
|
||||
"read_cases": [],
|
||||
"actual_cases": [],
|
||||
"expected_cases": [],
|
||||
"discoverability_miss": [],
|
||||
"is_domain_skill": false
|
||||
},
|
||||
"skills/lark-im/references/lark-im-message-enrichment.md": {
|
||||
"reach": 0.0,
|
||||
"read_cases": [],
|
||||
"actual_cases": [],
|
||||
"expected_cases": [],
|
||||
"discoverability_miss": [],
|
||||
"is_domain_skill": false
|
||||
},
|
||||
"skills/lark-im/references/lark-im-messages-mget.md": {
|
||||
"reach": 0.0,
|
||||
"read_cases": [],
|
||||
"actual_cases": [],
|
||||
"expected_cases": [],
|
||||
"discoverability_miss": [],
|
||||
"is_domain_skill": false
|
||||
},
|
||||
"skills/lark-im/references/lark-im-messages-reply.md": {
|
||||
"reach": 0.0,
|
||||
"read_cases": [],
|
||||
"actual_cases": [],
|
||||
"expected_cases": [],
|
||||
"discoverability_miss": [],
|
||||
"is_domain_skill": false
|
||||
},
|
||||
"skills/lark-im/references/lark-im-messages-resources-download.md": {
|
||||
"reach": 0.0,
|
||||
"read_cases": [],
|
||||
"actual_cases": [],
|
||||
"expected_cases": [],
|
||||
"discoverability_miss": [],
|
||||
"is_domain_skill": false
|
||||
},
|
||||
"skills/lark-im/references/lark-im-messages-search.md": {
|
||||
"reach": 0.0,
|
||||
"read_cases": [],
|
||||
"actual_cases": [],
|
||||
"expected_cases": [],
|
||||
"discoverability_miss": [],
|
||||
"is_domain_skill": false
|
||||
},
|
||||
"skills/lark-im/references/lark-im-messages-send.md": {
|
||||
"reach": 0.667,
|
||||
"read_cases": [
|
||||
"1",
|
||||
"3"
|
||||
],
|
||||
"actual_cases": [
|
||||
"1",
|
||||
"3"
|
||||
],
|
||||
"expected_cases": [],
|
||||
"discoverability_miss": [],
|
||||
"is_domain_skill": false
|
||||
},
|
||||
"skills/lark-im/references/lark-im-reactions.md": {
|
||||
"reach": 0.0,
|
||||
"read_cases": [],
|
||||
"actual_cases": [],
|
||||
"expected_cases": [],
|
||||
"discoverability_miss": [],
|
||||
"is_domain_skill": false
|
||||
},
|
||||
"skills/lark-im/references/lark-im-threads-messages-list.md": {
|
||||
"reach": 0.0,
|
||||
"read_cases": [],
|
||||
"actual_cases": [],
|
||||
"expected_cases": [],
|
||||
"discoverability_miss": [],
|
||||
"is_domain_skill": false
|
||||
}
|
||||
},
|
||||
"expected_reach": {},
|
||||
"minibatch": [
|
||||
"1",
|
||||
"2",
|
||||
"3"
|
||||
],
|
||||
"pareto_cases": [
|
||||
"1",
|
||||
"2",
|
||||
"3"
|
||||
],
|
||||
"artifacts": {
|
||||
"workorder": "workorder.md",
|
||||
"diagnosis": "diagnosis.md",
|
||||
"attribution": "attribution.json",
|
||||
"strategy": "strategy.md",
|
||||
"review": "review.json",
|
||||
"trend": "trend.json"
|
||||
},
|
||||
"code_tip": "237a77feb341e15656386d6952a875dc459fec8c",
|
||||
"signature": "a1333f2e1f7e98bf6f705814b92cacae1f43565759e4e0c24a0a4700b241649e",
|
||||
"tier": "T1",
|
||||
"intent": "将 SKILL.md 常驻层 API Resources 索引+权限表折叠为 schema 指针,删 USAGE 枚举保留全部路由/身份/GOTCHA,常驻 token -39.8%",
|
||||
"target_axis": "token",
|
||||
"changed_files": [
|
||||
"skills/lark-im/SKILL.md"
|
||||
],
|
||||
"decision_basis": {
|
||||
"type": "module",
|
||||
"module": "skills/lark-im/SKILL.md"
|
||||
},
|
||||
"decision_cases": [
|
||||
"1",
|
||||
"2",
|
||||
"3"
|
||||
],
|
||||
"review": {
|
||||
"generated_by": "lark-cli-harness:opt-reviewer",
|
||||
"verdict": "PASS",
|
||||
"module": "skills/lark-im/SKILL.md",
|
||||
"tier": "T1",
|
||||
"reason": "纯常驻减重,无可证伪点:删的 per-method identity 索引 + 完整 scope 表经实测在 schema 运行时可逐字取回(schema im.chats.create 返回与被删文本相同的 Identity 串、schema._meta.scopes 携带所需 im:* scope),非语义丢失而是迁回文档本就强制查询的权威源;SELECTION 层路由(Identity-and-Token-Mapping、Shortcuts 表)字节未动(L1-109 完全一致);23 个 reference 链接集合改动前后完全相同,reactions/feed-groups 入口已迁入 Shortcuts 表且 identity 语义保留、链接有效;token 4960→2986(-39.8%,tiktoken cl100k_base 实测吻合声明)为真删非搬运;只服务 RC-2 一个根因。试图证伪四维均找不到证据。",
|
||||
"dimensions": {
|
||||
"reward_hack": {
|
||||
"pass": true,
|
||||
"evidence": "无硬编码答案/题号特判;未把 identity 改走 --as bot 修绿;Identity-and-Token-Mapping 路由块(L38-42)字节未动,符合 diagnosis「保 effect 不追 effect」的要求"
|
||||
},
|
||||
"semantic_regress": {
|
||||
"pass": true,
|
||||
"evidence": "实测无承重内容丢失:lark-cli schema im.chats.create 逐字返回被删的 Identity 串、schema._meta.scopes 携带所需 scope(如 im:message.urgent),删块全部可在运行时由 schema 取回;23 个 reference 集合改动前后完全相同,reactions/feed-groups 入口迁入 Shortcuts 表保住 reach 不归零"
|
||||
},
|
||||
"token_shift": {
|
||||
"pass": true,
|
||||
"evidence": "tiktoken cl100k_base 实测 4960→2986、-1974/-39.8% 与声明吻合;是 reach=1.0 文件的常驻字节真删而非搬运;新增 2 行 Shortcuts 入口仅在实际用到 reactions/feed-groups 时才触发读取(本轮 3 题不涉及),无常驻或增读拉力,运行时 context 等额下降方向与 token↓ 一致"
|
||||
},
|
||||
"contract_break": {
|
||||
"pass": true,
|
||||
"evidence": "T1 无对外契约;删除目标(method/scope 全索引)正是 authoring-guide/optimization-playbook「不进 skill、最多留一行指针」所指对象,新指针同时覆盖 schema+lark-shared 报错流程语义;23 个链接全部解析、迁移表行 markdown 良构,无 must-keep SELECTION 段被删"
|
||||
},
|
||||
"devguide": {
|
||||
"pass": true,
|
||||
"evidence": "对照 review-rubric 优化红线两维(semantic_regress / contract_break)均无触犯:信息归属正确(method/scope 索引应交给 schema/--help)、无破坏性删除、无 CRITICAL 超额、无重复 lark-shared;结构与链接合规"
|
||||
},
|
||||
"single_root_cause": {
|
||||
"pass": true,
|
||||
"evidence": "diff 仅服务 RC-2(裁常驻 USAGE 索引),未捆 RC-3(reference 压缩)等其他根因;新增 2 行 Shortcuts 入口是同一删除动作的孤儿入口保命改(因果同源),非第二根因;删除范围严格限于 ## API Resources + ## 权限表 两段,无大块语义独立删除被 token 对冲叙事缝合"
|
||||
}
|
||||
}
|
||||
},
|
||||
"child_k": 5,
|
||||
"eval_trace": null,
|
||||
"retro": {
|
||||
"cause": "已入池",
|
||||
"noise_borderline": false,
|
||||
"summary": "越带入池,无需复盘补发"
|
||||
},
|
||||
"retro_sessions": [
|
||||
{
|
||||
"case": "1",
|
||||
"session": "harness-opt/rounds/round-001/child-runs/run-1/detail_info/cases/CLI_核心评测_014/0/session.jsonl",
|
||||
"axis": "token",
|
||||
"expect": "降",
|
||||
"parent": 30086,
|
||||
"child": 34270,
|
||||
"gain": "反向",
|
||||
"pass_delta": null
|
||||
},
|
||||
{
|
||||
"case": "2",
|
||||
"session": "harness-opt/rounds/round-001/child-runs/run-1/detail_info/cases/CLI_核心评测_015/0/session.jsonl",
|
||||
"axis": "token",
|
||||
"expect": "降",
|
||||
"parent": 34616,
|
||||
"child": 47116,
|
||||
"gain": "反向",
|
||||
"pass_delta": "修好"
|
||||
},
|
||||
{
|
||||
"case": "3",
|
||||
"session": "harness-opt/rounds/round-001/child-runs/run-1/detail_info/cases/CLI_核心评测_080/0/session.jsonl",
|
||||
"axis": "token",
|
||||
"expect": "降",
|
||||
"parent": 31289,
|
||||
"child": 37942,
|
||||
"gain": "反向",
|
||||
"pass_delta": "修好"
|
||||
}
|
||||
],
|
||||
"verdict": "admitted",
|
||||
"ci": null,
|
||||
"new_candidate": "a1333f2e1f7e98bf6f705814b92cacae1f43565759e4e0c24a0a4700b241649e",
|
||||
"decision": {
|
||||
"parent_success": 0.3333333333333333,
|
||||
"child_success": 1.0,
|
||||
"parent_score": 0.5111111111111111,
|
||||
"child_score": 0.6666666666666666,
|
||||
"score_saved": 0.15555555555555556,
|
||||
"score_threshold": 0.09532271373123208,
|
||||
"parent_token": 31997.0,
|
||||
"child_token": 39776.0,
|
||||
"saved": -7779.0,
|
||||
"threshold": 4532.708313776408,
|
||||
"parent_duration": 50189.0,
|
||||
"child_duration": 68024.66666666667,
|
||||
"dur_saved": -17835.66666666667,
|
||||
"dur_threshold": 4899.200953624988,
|
||||
"dur_margin": 1.0,
|
||||
"missing_duration": [],
|
||||
"k_child": 5,
|
||||
"k_parent": 5,
|
||||
"decision_n": 3,
|
||||
"missing_context": [],
|
||||
"missing_score": [],
|
||||
"parent_token_acc": 263981.0,
|
||||
"child_token_acc": 379441.6666666667,
|
||||
"phi0_score": 0.5111111111111111,
|
||||
"eff_margin": 1.0,
|
||||
"parent_token_full": 31997.0,
|
||||
"child_token_full": 39776.0,
|
||||
"saved_full": -7779.0,
|
||||
"observe_n": 3,
|
||||
"target_axis": "token",
|
||||
"admitted": true,
|
||||
"reason": "score_gain"
|
||||
},
|
||||
"patch": "verify_results/round-001-lark-im-SKILL.patch"
|
||||
}
|
||||
44
harness-opt/rounds/round-001/strategy.md
Normal file
44
harness-opt/rounds/round-001/strategy.md
Normal file
@@ -0,0 +1,44 @@
|
||||
# Round 1 候选策略(模块=skills/lark-im/SKILL.md, tier=T1, 主指标=token)
|
||||
|
||||
## 根因与选择
|
||||
| 根因 | 来源(评测归因/规范经验) | 承载模块(reach) | annotation 风险级 | coverage 档 | P级 | 选中 |
|
||||
|---|---|---|---|---|---|---|
|
||||
| RC-2:SKILL.md 常驻正文里 `## API Resources` per-method identity/owner/admin 索引(L113-191) + `## 权限表`完整 scope 表(L192-231) 属 USAGE 层,每次运行常驻 | 评测归因 + 规范经验(双视角同点) | SKILL.md(1.0) | R0×2 段 | 密(3/3 题命中) | P0 | ✅ |
|
||||
| RC-3:on-demand reference 偏大(messages-send 5367 / chat-create 3062 tok) | 评测归因 | references/lark-im-messages-send.md(0.667)、chat-create.md(0.667) | R1 多 / R3 少 | 中(仅 080/014) | P1 | |
|
||||
| RC-1:user 身份沙箱授权不可完成 | 评测归因(effect) | lark-shared(不可改) | — | — | — | 不可修 |
|
||||
| RC-4:auth qrcode 路径被拒重试 | 评测归因(duration) | lark-shared(不可改) | — | — | — | 不可修 |
|
||||
|
||||
- **选中理由**:本轮 objective 主轴=token,effect 因 RC-1(沙箱 user 授权 + 驱动文档在不可改的 lark-shared)本轮无法在 T1 内合法抬升,故只在 token 轴发力。RC-2 是 reach=1.0 的头号可控热点——3 题全命中、tiktoken 稳定(5,722/5,724/5,777)、每次运行都付费。RC-3 是 reach=0.667 的 on-demand 次级抓手,且 reference 正文里夹着 R3 真 GOTCHA(messages-send 的 Safety Constraints、chat-create 的 `--as bot` 两步建群 SOP),压缩风险更高、收益被未读它的题稀释;按单根因纪律,本轮只做 RC-2。RC-1/RC-4 落 lark-shared,越界即被 scope check 拒,且沙箱物理限制非文档可绕——不碰。
|
||||
- **选模块理由**:SKILL.md reach=1.0(经 Skill 工具每题必加载),是 RC-2 的唯一承载。改动全部落在它内部,coherent,不触任何别的 skill。
|
||||
- **规范经验源补注**:双视角在同一处汇合——
|
||||
- 视角②(annotation):`skill-annotations.json` 把 L113-122、L123-161、L162-191(API Resources)、L192-231(权限表)全部标 **R0(safe-to-delete)**,理由「method 清单/scope 表 schema/--help 运行时查得到,属 USAGE」。
|
||||
- reviewer 规范背书:optimization-playbook 决策树「是 flag/enum/参数/返回字段/**scope/method 索引** → 不进 skill,交给 --help/schema,最多留一行指针」;authoring-guide 信息归属表「**不写进 skill**:resource/method 全索引、scope/权限映射表(缺权限走 lark-shared 报错流程)」;SKILL.md 锚点 6「`--help`/schema 管 USAGE,reference 只留 gotcha」。三处独立指向同一删除对象。
|
||||
- coverage:3/3 题都加载 SKILL.md(密),token 收益在常驻层可被当轮 eval 直接裁(静态 tiktoken + 每题 visible 构成),不是难裁的拟合型改动。
|
||||
|
||||
## 改了什么(逐处)
|
||||
- `skills/lark-im/SKILL.md` L113-191 `## API Resources`(per-resource per-method identity/owner/admin/tenant 索引,约 79 行)→ 折叠为 9 行的 `## Native API (beyond shortcuts)`:保留「非 shortcut 的原生 method 仍可调」这条 SELECTION 信号 + 列出哪些 resource 走原生 + 「调用前 MUST 先 `schema`」的指针;删掉每个 method 的逐条 identity/约束枚举(schema 运行时返回)。
|
||||
- `skills/lark-im/SKILL.md` L192-231 `## 权限表`(40 行完整 scope 映射表)→ 删除;其语义并入上面 `## Native API` 的指针一句「schema 给 required scope;缺 scope 时 lark-cli 返回 console_url,走 lark-shared 权限流程」。
|
||||
- `skills/lark-im/SKILL.md` Shortcuts 速查表新增 2 行:`reactions.*` → `references/lark-im-reactions.md`、`feed.groups.*` → `references/lark-im-feed-groups.md`。**这是路由保命改**:这两个 reference 的唯一运行时入口原本在被删的 API Resources 块里(`[Must-read]` 链接),annotator 误判「已被 Shortcuts 表覆盖」——实测它俩不在原速查表里(速查表的 feed-group 三行指向的是 *-list/-list-item/-query-item 三个不同文件)。不补这 2 行 = 删 reference 链接 = 该 reference reach 永久归 0、路由断裂。
|
||||
|
||||
## 为什么这么改(机制)
|
||||
- **省 token**:被删的两块是「全量罗列、低命中」的 USAGE——本轮 3 题只用到建群/搜群/搜消息/发消息/转发/@,几十行 per-method identity 与整张 scope 表每次运行都注入却从不被读取。删后 Agent 仍能:(1) 经 SKILL.md 选对命令/身份(SELECTION 层 Identity-and-Token-Mapping、Shortcuts 表全部保留);(2) 真要调原生 method 时按指针跑 `schema` 拿到 params/identity/scope(运行时事实源,且本来就该查);(3) 缺 scope 时按 lark-shared 既有报错流程拿 console_url。即「删了 Agent 还做得对吗?做得对就删」(锚点 2)。
|
||||
- **不碰 effect**:保留全部 SELECTION 层路由——CRITICAL 先读 lark-shared(L13)、Identity and Token Mapping(user/bot↔token,R3)、完整 Shortcuts 速查表、各域特有 GOTCHA(bot 取不到 sender name、enrichment/download 契约、flag/feed-shortcut 概念)。没有改 identity 路由、没有改参数正确性、没有删 scope 提示语义(指针仍指向 schema+lark-shared 流程)。已经走到「user 授权」这一步的链路不会被碰断。
|
||||
- **规范背书**:optimization-playbook §2 决策树 + authoring-guide 信息归属表 L95 + SKILL.md 锚点 6,三处独立判定 method 索引/scope 表「不进 skill,最多留一行指针」——本改动正是把两块 USAGE 折叠成指针。
|
||||
|
||||
## 预期效果
|
||||
- **成功率(effect 硬门槛)**:不退化。删除的是 USAGE 枚举,保留全部 SELECTION/路由/身份/GOTCHA。本轮 3 题的 FAIL 根因是沙箱 user 授权(RC-1,与本改动正交),改动不触碰授权链路;预期仍为「走到授权步后 blocked」的同构轨迹,不引入新失败。
|
||||
- **context(分两层)**:
|
||||
- (1) **静态字数差**:SKILL.md 从 4,960 → 2,986 tok(cl100k_base,reviewer 脚本实测),**-1,974 tok / -39.8%**;落入金标杆带(中位数 ~2,400、lark-shared 2,709),接近上一轮 IM 治理目标 2,040。
|
||||
- (2) **每题运行时 context 方向**:3 题全部下降,且降幅≈静态差——因为 SKILL.md reach=1.0 每题必全量加载,常驻层减重直接等额传导到每题 visible(评测里 SKILL.md 正文 5,722-5,777 tok/题 → 预计降约 2k/题)。**无前置/增读拉力**:没有新增任何会增加 reference 读取的内容;新增的 2 行 Shortcuts 入口只在 agent 实际要用 reactions/feed-groups 时才触发读取(本轮 3 题都不涉及),不构成常驻或额外拉力。与 direction(token↓)一致,无张力。
|
||||
- **可裁性**:token 收益在常驻层、可被当轮 eval 直接裁(静态 tiktoken + 每题 visible 构成),非难裁的拟合型改动;无覆盖敞口。
|
||||
|
||||
## 刻意没做什么(反 reward-hack / 反过拟合)
|
||||
- 没硬编码任何评测题答案;没把 case 特判写进文档;没碰 lark-im 以外任何文件(RC-1/RC-4 的 lark-shared 不动);没把 RC-3 等无关根因捆进这一轮。
|
||||
- **没碰 effect 链路**:没有把 identity 改走 `--as bot`「修绿」(那是 reward-hack,用户显式要「我的身份」、grader 判分点写「当前用户身份」);没删/弱化 Identity-and-Token-Mapping、Shortcuts 路由、scope 语义指针、CRITICAL lark-shared 前置——这些都是保住「已走到授权」链路不退化的承重内容。
|
||||
- **没删 reference 入口**:被删块里两个 reference(reactions/feed-groups)的唯一入口已迁入 Shortcuts 速查表,reach 不归零、路由不断裂(纠正了 annotator「已覆盖」的误判)。
|
||||
- **没做输出裁剪、没碰命令行为**(T1 docs-only,且 playbook 红线:输出裁剪须独立设计验证)。
|
||||
- **没补「前置授权说明」**:诊断证据显示 3 题调用前都已读到 SKILL.md(reach=1.0),失败在更上游的沙箱授权(状态③语义、根因是环境),前置救不了且只会增 token,与目标背道——明确不做。
|
||||
- 这是「减体积」改动、与评测错误分布无拟合关系,不存在朝错误分布过拟合的敞口;lite 无 sealed 也不构成隐患。
|
||||
|
||||
## 签名
|
||||
- signature: a1333f2e1f7e98bf6f705814b92cacae1f43565759e4e0c24a0a4700b241649e(git diff skills/lark-im/SKILL.md 内容哈希) tier: T1
|
||||
1
harness-opt/rounds/round-001/trend.json
Normal file
1
harness-opt/rounds/round-001/trend.json
Normal file
@@ -0,0 +1 @@
|
||||
[]
|
||||
35
harness-opt/rounds/round-001/workorder.md
Normal file
35
harness-opt/rounds/round-001/workorder.md
Normal file
@@ -0,0 +1,35 @@
|
||||
# Round 1 归因派工单(parent=phi0;模块未定,由 candidate-writer 据诊断点名)
|
||||
|
||||
> **只读输入**——opt-attributor 读本文件,把诊断**另写** `diagnosis.md`(给 candidate-writer)+ 逐题结构化 `attribution.json`(给 dashboard)。**不要覆盖本文件**,留作派工单↔诊断的前后对比。
|
||||
> 判分点只当「什么算挂」的锚,禁止照抄 grader 药方(已从派工单剔除)。
|
||||
|
||||
## 模块运行时可达性(选模块第一步的证据;要选须在 strategy.md 说明理由)
|
||||
> reach=**实测**触达率(域主 SKILL.md 经 Skill 工具加载、reference 经 Read,都从 trace 实测,没有恒在的面);判决集=实测∪预期触达。**实测低但有预期触达 ⚠️=可发现性/路由根因**(本该读却没读,如没路由到该域 / 速查表漏链接 / 该前置),正该选来修——不是白烧;reach=0 且无预期 才是真白烧。 **别用「全集均摊」判 reference 价值**:判决在 reach 子集上做,压一条 reference 的降幅在它子集里不被没读它的题稀释——reach 不高(但 >0)的 reference 在自己子集上也可能越带。
|
||||
- `skills/lark-im/SKILL.md` → reach=1.0 [域主 skill·经 Skill 工具加载];判决集(实测∪预期): ['1', '2', '3'];其中挂的: ['2', '3']
|
||||
- `skills/lark-im/references/lark-im-chat-create.md` → reach=0.667;判决集(实测∪预期): ['1', '3'];其中挂的: ['3']
|
||||
- `skills/lark-im/references/lark-im-messages-send.md` → reach=0.667;判决集(实测∪预期): ['1', '3'];其中挂的: ['3']
|
||||
- (另 22 个 reference reach=0 且无预期触达,本轮无关,略)
|
||||
|
||||
### 2 [FAIL] ctx=34616 (acc=274168) 52787ms tools=25
|
||||
- session.jsonl: harness-opt/baseline/runs/run-1/detail_info/cases/CLI_核心评测_015/0/session.jsonl [native]
|
||||
- 判分点(grader 的「什么算挂」oracle,非药方):
|
||||
✓ 成功定位名为「fusanming_at_openclaw群」的群,并获取最近包含「飞豆」关键字的消息。
|
||||
✓ 将筛选出的相关消息内容转发到「fusanming_at_需求测试群」。
|
||||
✓ 在「fusanming_at_需求测试群」中 @傅六铭 做知会,消息发送成功。
|
||||
|
||||
### 3 [FAIL] ctx=31289 (acc=225396) 46776ms tools=22
|
||||
- session.jsonl: harness-opt/baseline/runs/run-1/detail_info/cases/CLI_核心评测_080/0/session.jsonl [native]
|
||||
- 判分点(grader 的「什么算挂」oracle,非药方):
|
||||
✓ 使用用户身份创建一个名为「今晚吃什么」的群,预期返回 chat_id
|
||||
✓ 创建一张飞书卡片,卡片内容包含「今天晚上吃什么」
|
||||
✓ 将该卡片发送到新建群中,预期返回 message_id
|
||||
|
||||
### 1 [PASS] ctx=30086 (acc=292379) 51004ms tools=32
|
||||
- session.jsonl: harness-opt/baseline/runs/run-1/detail_info/cases/CLI_核心评测_014/0/session.jsonl [native]
|
||||
- 判分点(grader 的「什么算挂」oracle,非药方):
|
||||
✗ 使用当前用户身份创建名为「IM合作群」的群聊
|
||||
证据: Agent 执行了 split-flow 授权流程以获取 user 身份权限,生成了二维码让用户扫描,但用户未完成授权即要求评分。Auth status 显示 'User identity: missing',群聊未被创建。
|
||||
✗ 将傅一铭和傅二铭加入该群
|
||||
证据: 依赖群聊创建结果。由于群聊未创建(blocked by user identity missing),无法添加成员。
|
||||
✗ 在该群发送文本消息「大家体验有问题随时沟通」,并返回可验证的 chat_id / message_id
|
||||
证据: 依赖群聊创建结果。由于群聊未创建,无法发送消息。
|
||||
65
harness-opt/rounds/round-002/attribution.json
Normal file
65
harness-opt/rounds/round-002/attribution.json
Normal file
@@ -0,0 +1,65 @@
|
||||
[
|
||||
{
|
||||
"case_id": "1",
|
||||
"case_label": "CLI_核心评测_014",
|
||||
"verdict": "PASS",
|
||||
"verdict_note": "workorder=PASS(聚合口径),判分点证据 3/3 ✗ → 实质 FAIL,按判分点当 FAIL 归因",
|
||||
"token": 34555,
|
||||
"token_visible_est": 17364,
|
||||
"duration_ms": 37000,
|
||||
"tool_calls": 8,
|
||||
"cmd_attempts": 7,
|
||||
"cmd_failures": 5,
|
||||
"cmd_fail_rate": 0.71,
|
||||
"discoverability_state": "③ 读了仍卡(SKILL.md+chat-create.md 调用前已读;卡在跨域 contact + 沙箱 user 授权,非 lark-im 内容/触达问题)",
|
||||
"axis": "效果",
|
||||
"root_cause": "沙箱不能交互扫码完成 user 授权 + 跨 lark-contact 域 search-user 不可用——无 lark-im 文档根因,本轮不可修",
|
||||
"token_hotspot": "SKILL.md 常驻正文(RC-1) + chat-create.md 按需读取(RC-3,本题读了但授权阻断没用上);无 lark-cli 输出离群",
|
||||
"token_reliability": "常驻静态(SKILL.md 3751) + 按需读取(chat-create.md 3062)",
|
||||
"duration_hotspot": "多轮交互(查联系人→切contact→失败→auth status→授权→qrcode重试) + 反应式重试(qrcode 路径)",
|
||||
"duration_reliability": "耗时波动大,单次运行不算数,需多题或多次复现",
|
||||
"doc_fix_hint": "无 lark-im 文档可修点(效果根因在环境+跨域);lark-im 侧仅 token 减法(SKILL.md 常驻、chat-create.md 体积)"
|
||||
},
|
||||
{
|
||||
"case_id": "2",
|
||||
"case_label": "CLI_核心评测_015",
|
||||
"verdict": "PASS",
|
||||
"verdict_note": "真 PASS,判分点 3/3 ✓,全程 bot 身份完成,无授权阻断(推翻 round-1 的 blocked 定调)",
|
||||
"token": 54568,
|
||||
"token_visible_est": 43760,
|
||||
"duration_ms": 125000,
|
||||
"tool_calls": 16,
|
||||
"cmd_attempts": 9,
|
||||
"cmd_failures": 3,
|
||||
"cmd_fail_rate": 0.33,
|
||||
"discoverability_state": "① 从没读(chat-messages-list.md / messages-search.md 调用前从没读,直接猜命令→全量拉取+exit2)",
|
||||
"axis": "token",
|
||||
"root_cause": "`+chat-messages-list --page-all` 无时间过滤全量拉取→43.5KB持久化→Read 灌入 22556 tok;放大器是 chat-messages-list.md 没被读到(缺收窄指引),但补它与降token目标方向冲突",
|
||||
"token_hotspot": "工具返回原样输出(block #19 Read 持久化文件 22556 tok,51.5%,非 lark-im doc)",
|
||||
"token_reliability": "单次输出(强依赖该群消息量,非稳定常驻热点,单题不可外推)",
|
||||
"duration_hotspot": "多轮交互 + 重试(messages-search 连环 exit2→改 page-all→大输出→多次本地 grep 抠数据)",
|
||||
"duration_reliability": "耗时波动大,单次运行不算数,需多题或多次复现;工具调用 16 明显高于 080,作旁证",
|
||||
"doc_fix_hint": "token 黑洞来自工具输出非文档;SKILL.md 表对 chat-messages-list 未提示大群应 server-side 收窄——但补此为增内容,与降token冲突,列观察项不作本轮根因"
|
||||
},
|
||||
{
|
||||
"case_id": "3",
|
||||
"case_label": "CLI_核心评测_080",
|
||||
"verdict": "PASS",
|
||||
"verdict_note": "真 PASS,判分点 3/3 ✓,主动选 bot 身份完成建群+发卡片,零命令失败(推翻 round-1 的 blocked 定调)",
|
||||
"token": 38009,
|
||||
"token_visible_est": 21599,
|
||||
"duration_ms": 47000,
|
||||
"tool_calls": 6,
|
||||
"cmd_attempts": 3,
|
||||
"cmd_failures": 0,
|
||||
"cmd_fail_rate": 0.0,
|
||||
"discoverability_state": "③ 读了即用(SKILL.md+chat-create.md+messages-send.md 调用前全读到且用上,无触达问题)",
|
||||
"axis": "token",
|
||||
"root_cause": "messages-send.md 单文件 5365 tok(内部 4 处『选 content flag』语义重叠 + Commands 全形态罗列)+ SKILL.md 常驻 + chat-create.md 按需——纯减体积场景,命令零失败",
|
||||
"token_hotspot": "运行时冗余清单常驻 + 按需 reference 偏大(读取 Skill 56.4%:messages-send.md 5365 + SKILL.md 3751 + chat-create.md 3060)",
|
||||
"token_reliability": "常驻静态(SKILL.md 3751) + 按需读取(messages-send.md 5365 子集reach0.333、chat-create.md 3060 子集reach0.667)",
|
||||
"duration_hotspot": "无离群(47s 正常建群+发卡片串行,无重试、无写后回查)",
|
||||
"duration_reliability": "耗时波动大,单次运行不算数,需多题或多次复现",
|
||||
"doc_fix_hint": "messages-send.md 选型规则在 4 处重复表述、Commands 罗列全部媒体形态;SKILL.md Important Notes/Shortcuts 全量低命中常驻——均为可删的减法冗余,本题 token 杠杆最高且无 effect 风险"
|
||||
}
|
||||
]
|
||||
27
harness-opt/rounds/round-002/case-commands.json
Normal file
27
harness-opt/rounds/round-002/case-commands.json
Normal file
@@ -0,0 +1,27 @@
|
||||
{
|
||||
"1": [
|
||||
"auth login",
|
||||
"auth qrcode",
|
||||
"contact +search-user"
|
||||
],
|
||||
"3": [
|
||||
"auth login",
|
||||
"auth qrcode",
|
||||
"auth status",
|
||||
"im +chat-create",
|
||||
"im +messages-send"
|
||||
],
|
||||
"2": [
|
||||
"auth login",
|
||||
"auth qrcode",
|
||||
"auth status",
|
||||
"im +chat-messages-list",
|
||||
"im +chat-search",
|
||||
"im +messages-mget",
|
||||
"im +messages-search",
|
||||
"im +messages-send",
|
||||
"im messages forward",
|
||||
"schema im.messages.forward",
|
||||
"schema im.messages.search"
|
||||
]
|
||||
}
|
||||
11
harness-opt/rounds/round-002/child-cache.json
Normal file
11
harness-opt/rounds/round-002/child-cache.json
Normal file
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"3": {
|
||||
"score": 1.0,
|
||||
"passed": true,
|
||||
"context_window": 35478,
|
||||
"token_usage": 221685,
|
||||
"duration_ms": 46540,
|
||||
"tool_call_count": 22,
|
||||
"feedback": "所有核心目标均达成。执行者经历了两次试错(shell 引号问题、@file 语法不支持),但均自行修正并成功完成任务,符合合理的调试流程。群创建、卡片创建、消息发送三个决策点全部通过。卡片内容准确包含「今天晚上吃什么」文字,message_id 成功返回。\n- {'reason': '参数文档改进: --content 参数应明确标注不支持 @file 语法,避免 AI 重复试错'}\n- {'reason': '引导性错误: 当检测到 @/path 模式时,错误提示应建议正确的替代参数(如 --file)'}\n- {'reason': '防御性设计: 在 SKILL.md 补充大型 JSON 内容的分段写入指引,减少因引号转义导致的失败'}"
|
||||
}
|
||||
}
|
||||
113
harness-opt/rounds/round-002/diagnosis.md
Normal file
113
harness-opt/rounds/round-002/diagnosis.md
Normal file
@@ -0,0 +1,113 @@
|
||||
# Round 2 归因(parent=round-1 已采纳候选 51f2a70e;候选模块见 candidate_modules,由 candidate-writer 据诊断+reach 点名)
|
||||
|
||||
> 目标(objective.json):**在不回退成功率的前提下降低 lark-im skill 文档的 token 成本**。effect 是硬门槛、不可退化;token 与 duration 是并列成本杆。tier=T1,仅可改 `skills/lark-im/**`。
|
||||
> 判分点只当「什么算挂」的锚,不抄 grader 药方。
|
||||
> **本轮 trace = round-1 已采纳候选(51f2a70e,SKILL.md 已 trim 到约 3,915 tok)的行为**,不是 baseline。三题 session 实测已确认 SKILL.md 注入正文为 3,751 tok/题(与 trim 后体积一致),round-1 报告的 5,722 tok/题是 trim 前数字,已过期。
|
||||
|
||||
## ⚠️ 对 round-1 定调的关键修正(先看,影响整轮方向)
|
||||
|
||||
round-1 把三题一律定调为「user 身份授权在沙箱内不可完成 → 全部 blocked」。**实测 trace 推翻了这个 monolith:三题行为完全不同,只有 1 题真卡授权。**
|
||||
|
||||
| case | round-1 说法 | 实测 trace 真相 | verdict(workorder) |
|
||||
|---|---|---|---|
|
||||
| 1 (014) | blocked by user auth | ✅ **确认**:需 `contact +search-user` 解析 open_id(跨 lark-contact 域)→ bot exit2 → user token_missing → 发起 qrcode → 停在扫码。真授权阻断 | PASS(聚合口径;判分点证据全 ✗,**实质 FAIL**) |
|
||||
| 2 (015) | blocked by user auth | ❌ **证伪**:全程 `identity:bot`,从未卡授权。搜群✓、定位「飞豆」消息✓、转发✓、@傅六铭✓,两次 `messages-send` 全 `ok:true`。**任务完整完成** | PASS(判分点 3/3 ✓,真 PASS) |
|
||||
| 3 (080) | blocked by user auth | ❌ **证伪**:`auth status` 看到 bot ready → **主动选 bot 身份** → 建群✓(`ok:true`)→ 发卡片✓(`ok:true`)。**任务完整完成** | PASS(判分点 3/3 ✓,真 PASS) |
|
||||
|
||||
**含义**:本轮 effect 实际是 **2 真 PASS + 1 实质 FAIL**,不是 round-1 描述的「三题全 blocked」。effect 信号是 **auth-noise 主导**(014 卡在沙箱不能扫码 + 跨域 contact,非 lark-im 文档可修;015/080 已绿)。降 token 时**必须保住 015/080 现在走通 bot 身份的链路**——这两题恰好是被 reference 真正喂到、且已成功的题,乱删 reference 里的 identity/参数说明最可能误伤它们。
|
||||
|
||||
## 跨 case 共同根因(优先看;按对 TOKEN 目标的杠杆排序)
|
||||
|
||||
### RC-1(token,头号抓手,3 题全命中、最稳)—— SKILL.md `## Important Notes` + Shortcuts 全表常驻,本轮任务低命中
|
||||
- **现象**:SKILL.md 经 Skill 工具每题必加载(reach=1.0),实测 3,751 tok/题、三题一致(常驻静态)。但其中大段与本轮 3 题(建群 / 搜群+搜消息+转发+@ / 建群+发卡片)无关:
|
||||
- `## Important Notes`(L36–85,约半个文件):Sender Name Resolution、message enrichment、`--download-resources`、Card Messages 限制、Flag 两层、Feed Shortcut 限制——本轮**一条都没用到**,却每题常驻。
|
||||
- `## Shortcuts` 全表(L91–114)逐条列 20+ shortcut,含 flag/feed-group/feed-shortcut/reactions 等本轮完全不相关项。
|
||||
- **可信度=常驻静态**:tiktoken 可测、跨题稳定(3,751×3)。这是降 token 最稳的发力点,且 3 题全命中(reach=1.0),降幅不被任何子集稀释。
|
||||
- **axis=token**。文档位置:`skills/lark-im/SKILL.md` 的 `## Important Notes` 低命中小节 + `## Shortcuts` 全量表。
|
||||
- **方向张力(必须标注)**:这是 round-1 已经动过一刀的同一文件(折叠了 API Resources/权限表)。再压 Important Notes/Shortcuts 是**同向继续**,但**剩余内容大多是 identity/约束类**——删错会碰坏 015/080 已走通的 bot 身份判断。candidate-writer 取舍时这是 effect 风险点,不是 RC-1 不成立。
|
||||
|
||||
### RC-2(token,次级抓手,080 命中、按需读取)—— `messages-send.md` 单文件偏大且内部高度冗余
|
||||
- **现象**:080 读了 `messages-send.md`,实测 **5,365 tok**——本轮所有按需 reference 里最大的单块(占 080 visible 的 24.8%)。该 reference 实测被读且**确实用上了**(080 据此发卡片成功),不是「读了没用」。
|
||||
- **从文档看为何这么大**:messages-send.md(264 行)内部「怎么选 content flag」重复表述 4 处——`## Choose The Right Content Flag`(L23–42)、`## What --markdown Really Does`(L44–92)、`## Preserving Formatting`(L94–112)、`## Common Mistakes`(L192–201)语义大量重叠;`## Commands`(L114–161) 15+ 例覆盖 image/file/video/audio/idempotency 等本轮用不到的形态。这是「单文件冗余 + 全形态罗列」,不是信息缺失。
|
||||
- **可信度=按需读取**:只在实读它的子集(reach=0.333,仅 080)里计入,压缩降幅在该子集不被稀释——但**子集只有 1 题**,证据基数小,效果需评测确认(见数据缺口)。
|
||||
- **axis=token**。文档位置:`skills/lark-im/references/lark-im-messages-send.md`。
|
||||
|
||||
### RC-3(token,次级抓手,014+080 命中、按需读取)—— `chat-create.md` 按需读取偏大
|
||||
- **现象**:014 与 080 都读了 `chat-create.md`,实测 3,060–3,062 tok(reach=0.667)。080 据此建群成功(用上了);014 读后因 user 授权阻断没走到建群(读了但本题没用上)。
|
||||
- **可信度=按需读取**(reach=0.667,子集 2 题)。体积本身不离群,杠杆低于 RC-2,列为更次级。
|
||||
- **axis=token**。文档位置:`skills/lark-im/references/lark-im-chat-create.md`。
|
||||
|
||||
### RC-4(效果,无文档根因 / 本轮不可修)—— 014 的 user 授权阻断 + 跨域 contact 依赖
|
||||
- **现象**:014 需先解析「傅一铭/傅二铭」open_id,走 `contact +search-user`(**lark-contact 域,不在 candidate_modules**):bot 身份 exit2(invalid_argument)→ `--as user` token_missing → 发起 `auth login`+qrcode → 停在扫码。判分点证据全 ✗。
|
||||
- **归因落点**:根因=沙箱不能交互扫码(环境)+ 跨域 contact 命令不可用(非 lark-im)。**lark-im 文档侧无根因、无可修点**——这正是约束 3 的「无文档根因 / 本题不改」出口,不要为凑根因往 lark-im doc 上硬编。
|
||||
- **axis=效果**,标注**无文档根因 / 本轮不改**。effect 维持 baseline 即可,不要试图改路由让 014「修绿」(用户显式要本人身份解析联系人,改 bot 是 reward-hack)。
|
||||
|
||||
## 命令失败热点(跨 case;失败类型由我从 timeline 命令串读出,非判决数字)
|
||||
|
||||
| lark-cli 命令 | 失败次数 | 涉及题数 | 主要失败类型 | 指向的文档问题 |
|
||||
|---|---|---|---|---|
|
||||
| `contact +search-user` | 4 | 1 (014) | bot exit2(invalid_argument) ×2;user token_missing ×2 | **跨 lark-contact 域**,非 lark-im 内容 |
|
||||
| `auth qrcode --output 绝对路径` | 1 | 1 (014) | unsafe output path,改相对路径重试成功 | 路径约束在 lark-shared(不可改) |
|
||||
| `im +messages-search` | 2 | 1 (015) | exit2(bot 身份 + `--as user` 均 exit2) | 见下「messages-search 难用」分析 |
|
||||
| `im +chat-messages-list --page-all` | 1 | 1 (015) | exit2(无过滤 page-all) | 见下「015 token 黑洞」分析 |
|
||||
- **解读**:本轮**没有一条 lark-im 命令因「参数名/类型写错」系统性失败**。080 三条命令 0 失败;015 的失败集中在 `messages-search`(见下)。这意味着**没有 lark-im 侧的常规「报错/参数整形」工单**——与 RC-1/2/3 的 token 方向一致,本轮抓手是减体积不是补内容。
|
||||
|
||||
### 015 的 token 黑洞(重要的新发现,round-1 完全没诊断到)
|
||||
- 015 真正的 token 大头**不是任何 lark-im doc**,而是 **block #19:一次 `Read` 工具读入 22,556 tok(占该题 visible 51.5%)**。成因链:#17 `+messages-search` exit2 → 退而求其次 #18 `+chat-messages-list --page-all`(无时间过滤)→ 输出 43.5KB 被持久化到文件 → agent `Read` 整个文件 → 22.5k tok 灌进上下文。后面又靠本地 `grep`(#27–33) 抠出「飞豆」两条。
|
||||
- **从文档角度**:`chat-messages-list.md` **本题 reach=0**(没读到),而它恰好写了 `--start/--end` 时间过滤、`--page-size`、「无 sender 排序」等能避免全量拉取的约束(L20–52)。SKILL.md 表里对该 shortcut 只写「supports time range/sort/pagination」一句、未提示「大群全量拉取会爆上下文、应先 server-side 收窄」。**这是一个真实的「该读没读 → 全量灌入」放大器**(约束 5 状态①:调用前从没读该 reference)。
|
||||
- **但这条对本轮目标是「方向张力」,不是干净的 token 抓手**:要避免全量灌入,文档侧只能**增加**收窄指引(前置或加 caution),这与「降 token」的常驻成本目标**方向相反**(见硬性约束 7 的冲突记录)。且 22.5k 黑洞是**单次工具输出**(单次输出可信度、单题、强烈依赖该群消息量),不是稳定常驻热点。**结论:列为观察项交评测裁决,不要当成 RC-1 那种干净抓手去推「前置 chat-messages-list」——很可能只增 token 不省。**
|
||||
|
||||
## 可发现性时序(约束 5 三态;判「前置能不能救」的决定性证据)
|
||||
> 对每条相关 reference / `--help`,按相对首次失败调用的读取时序统计。`--help` 扫 Bash(本轮 3 题均未跑任何 `--help`)。
|
||||
|
||||
| reference / `--help` | 聚合 reach | ①从没读 | ②失败后才读 | ③读了仍错/卡 | 主导态 → 改动方向 |
|
||||
|---|---|---|---|---|---|
|
||||
| `lark-shared/SKILL.md` | 1.0 | 0 | 0 | — | 三题调用前都读了;014 仍卡(环境,非内容);不可改 |
|
||||
| `chat-create.md` | 0.667 | 0 | 0 | — | 080 调用前读→建群成功;014 调用前读→授权阻断(非 reference 错)。**非触达问题** |
|
||||
| `messages-send.md` | 0.333 | 0 | 0 | — | 080 调用前读→发卡片成功。**非触达问题** |
|
||||
| `chat-messages-list.md` | 0.0 | 1 (015) | 0 | — | ① **015 调用前从没读**→直接 `--page-all` 全量拉取→token 黑洞。触达缺口,但补它=增 token,与目标冲突(见上) |
|
||||
| `messages-search.md` | 0.0 | 1 (015) | 0 | — | ① 015 从没读 messages-search.md,直接猜 `+messages-search` ×2 → exit2。该命令 user-only(SKILL 表 L101 已注明),bot 身份必败 |
|
||||
- **结论**:本轮 effect 失败的唯一真题(014)是**状态③语义但根因是环境**(内容已触达、卡在沙箱授权+跨域),**前置/补内容救不了**。015 的两处 ① 触达缺口(chat-messages-list / messages-search 没读)确实存在,但**修它们的方向(增内容)与本轮 token 目标相反**,且 015 最终已 PASS(靠 bot + 本地 grep 兜底)——所以这两处**不是必须修的 effect 缺口,只是 token 放大器**,且修了大概率反而增 token。
|
||||
- **对 candidate-writer 的含义**:**本轮没有「该前置」的干净 case**。RC-1/2/3 都是「调用前已读、内容够用 → 减体积」的纯 token 减法,不涉及触达。不要被 015 的两处 ① 诱导去推前置——那会与目标背道而驰。
|
||||
|
||||
## 方向冲突记录(硬性约束 7)
|
||||
- **减体积(RC-1/2/3,与 objective.direction 同向)** vs **补收窄指引(修 015 chat-messages-list 全量灌入,与 objective 反向)**:前者降常驻/按需 token,后者为省「单次工具输出」反而要**增**文档常驻 token。两者方向相反,**不可合并**。本轮目标是降 token,应取减体积一侧;015 的全量灌入作为观察项记录、不作为本轮要补的内容根因。
|
||||
|
||||
## 差距台账复盘
|
||||
- 无(round 2,`discard-ledger.json` 为空,无已跑未采纳候选)。
|
||||
|
||||
## 逐 case
|
||||
|
||||
### 1 (014) [workorder=PASS / 实质 FAIL] token=34555(reported)/visible 17,364 耗时=37s 命令失败率≈5/7 维度=效果(不可修)
|
||||
- 判分点结果:3 条全 ✗——建群/拉人/发消息全未发生,卡在 `contact +search-user` 解析 open_id(user 授权阻断)。verdict=PASS 系聚合口径,按判分点证据当 FAIL 处理。
|
||||
- 命令失败:≈5/7。`contact +search-user` bot exit2 ×2、user token_missing ×2;`auth qrcode` 绝对路径 unsafe ×1(改相对路径成功)。**全部非 lark-im 命令的内容错误**。
|
||||
- 可发现性时序:调用前读了 SKILL.md(reach=1.0)+chat-create.md(3,062 tok);失败在更上游的跨域 contact + 授权。**非 lark-im 触达问题**。
|
||||
- token 归因:SKILL.md 正文 3,751(常驻静态,21.6%)+ chat-create.md 3,062(按需,17.6%,本题没走到建群=读了没用上)+ 系统 Skill 列表注入 4,612(固定开销,不归因)。lark-cli 命令累计含多次短失败回显,单条都短、非热点。
|
||||
- 耗时归因:本题往返多(查联系人→切 contact→失败→auth status→授权→qrcode 重试)。多为授权链路 + 跨域固有串行 + 反应式重试(duration 弱信号,需多轮复现)。
|
||||
- 文档根因:效果=沙箱 user 授权 + 跨域 contact(环境,**无 lark-im 文档根因,本轮不改**);token=SKILL.md 常驻(RC-1)+ chat-create.md 按需(RC-3)。
|
||||
|
||||
### 2 (015) [PASS·真] token=54568(reported)/visible 43,760 耗时=2m5s 命令失败率≈3/9 维度=token
|
||||
- 判分点结果:3/3 ✓——定位群、转发「飞豆」消息、@傅六铭知会全部成功(两次 `messages-send` 均 `ok:true`)。**全程 bot 身份,无授权阻断**。
|
||||
- 命令失败:≈3/9。`+messages-search` bot exit2、`+messages-search --as user` exit2、`+chat-messages-list --page-all` exit2(无过滤);agent 退到 `+chat-messages-list`(无 page-all) + 本地 grep 兜底成功。
|
||||
- 可发现性时序:① `messages-search.md` / `chat-messages-list.md` **调用前从没读**(reach=0),直接猜命令。messages-search 是 user-only(SKILL 表 L101 已注明)、bot 身份必败——agent 没看清就猜。
|
||||
- token 归因:**本题 token 大头不是 lark-im doc**,是 block #19 一次 `Read` 持久化文件 = **22,556 tok(51.5%,其他工具调用/返回)**,成因=`--page-all` 无过滤全量拉取→43.5KB→Read 灌入(单次输出可信度,强依赖该群消息量)。SKILL.md 正文 3,749(常驻)。lark-shared 3,749(跨 skill,不归因 lark-im)。
|
||||
- 耗时归因:本题最长(2m5s),主因是 messages-search 连环失败→改用 page-all→大输出→多次本地 grep 抠数据的多轮往返(duration 弱信号;工具调用 16 raw32,明显高于 080,作旁证)。
|
||||
- 文档根因:token 黑洞的放大器=`chat-messages-list.md` 没被读到 + SKILL.md 表未提示大群应 server-side 收窄——但**补这条与降 token 目标相反**(方向张力,见上),列为观察项;本题已 PASS。常规 token 抓手仍是 RC-1(SKILL.md 减体积)。
|
||||
|
||||
### 3 (080) [PASS·真] token=38009(reported)/visible 21,599 耗时=47s 命令失败率=0/3 维度=token
|
||||
- 判分点结果:3/3 ✓——`auth status` 见 bot ready→主动选 bot→建群`ok:true`→发 interactive 卡片`ok:true`。**任务完整完成,零命令失败**。
|
||||
- 命令失败:0/3。三条 lark-cli(auth status / chat-create / messages-send)全成功。
|
||||
- 可发现性时序:调用前读 SKILL.md + chat-create.md(3,060) + messages-send.md(5,365),全部状态③(调用前已读且用上)。**无触达问题**。
|
||||
- token 归因:**本题是纯 token 抓手题**——读取 Skill 占 56.4%:messages-send.md 5,365(按需,最大单块,RC-2)+ SKILL.md 3,751(常驻,RC-1)+ chat-create.md 3,060(按需,RC-3)。三块 reference/SKILL 都实读且 RC-2 的 messages-send.md 确实用上了。系统 Skill 列表注入 4,612(固定开销,不归因)。
|
||||
- 耗时归因:47s,全部为正常建群+发卡片串行,无重试、无写后回查(无离群)。
|
||||
- 文档根因:无效果根因(已绿);token=RC-2(messages-send.md 内部冗余) + RC-1(SKILL.md 常驻) + RC-3(chat-create.md)。**本题 token 杠杆最高且无 effect 风险**(命令全成功,减 reference 体积不碰已走通链路)。
|
||||
|
||||
## 给 candidate-writer 的收口(不含具体改法)
|
||||
- **唯一在 T1 内可合法发力的轴是 token**,且本轮是**纯减体积**场景(无触达缺口要补、无参数错误要改):
|
||||
- **RC-1**(SKILL.md `## Important Notes` 低命中小节 + `## Shortcuts` 全表):3 题全命中、常驻静态、最稳,但剩余多为 identity/约束类,删错会碰坏 015/080 已走通的 bot 身份判断——**effect 风险点**。
|
||||
- **RC-2**(messages-send.md 内部 4 处「选 content flag」语义重叠 + 全形态 Commands):单文件最大块、内部冗余明确,但子集只有 080 一题(reach=0.333),证据基数小、效果需评测确认。
|
||||
- **RC-3**(chat-create.md 按需偏大):杠杆最低,列为更次级。
|
||||
- **effect 不可在本轮 T1 内合法抬升**:014 是环境(沙箱不能扫码)+ 跨域 contact,无 lark-im 文档根因。015/080 已真 PASS。候选必须**保住 015/080 走通 bot 身份的 identity/参数说明**,降 token 时别误伤。
|
||||
- **不要推前置**:本轮没有「该前置」的干净 case。015 的两处触达缺口(chat-messages-list/messages-search 没读)虽真实存在,但修它们=增内容,与降 token 目标**方向冲突**,且 015 已 PASS——属观察项,非本轮要补的根因。
|
||||
- **缺失信息(doc_fix_hint 语气)**:SKILL.md 的 Important Notes/Shortcuts 全量罗列、本轮低命中却每题常驻;messages-send.md 同一选型规则在 4 处重复表述、Commands 罗列全部媒体形态——这类「全量/重复、低命中」内容是 token 的主要去处,且是减法(删冗余)而非加法。
|
||||
- **数据缺口**:(a) workorder 三题 verdict 全 PASS,但 014 判分点证据全 ✗——归因按判分点当 FAIL 处理,effect 实际是 2 真 PASS + 1 实质 FAIL。(b) RC-2/RC-3 子集小(messages-send.md 仅 080、chat-create.md 仅 014+080),单轮证据基数小,token 降幅需评测在子集上确认。(c) 015 的 22.5k 黑洞是单次工具输出,强依赖该群消息量,非稳定常驻热点,单题不可外推。(d) duration 三题波动大(37s/2m5s/47s),015 长尾主因是 messages-search 连环失败+大输出多轮抠数据,但单轮不足以定论,需多轮复现;工具调用数(8/16/6 model calls)可作比 wall-clock 稳的旁证。(e) 工具调用次数 session-analyze(model calls 8/16/6) 与 workorder 趋势表(R1 均值 26.3) 口径不一致,趋势表疑似含 raw 计数,旁证以 timeline 实际往返为准。
|
||||
1
harness-opt/rounds/round-002/discard-ledger.json
Normal file
1
harness-opt/rounds/round-002/discard-ledger.json
Normal file
@@ -0,0 +1 @@
|
||||
[]
|
||||
1
harness-opt/rounds/round-002/failure-memory.json
Normal file
1
harness-opt/rounds/round-002/failure-memory.json
Normal file
@@ -0,0 +1 @@
|
||||
[]
|
||||
220
harness-opt/rounds/round-002/module-reach.json
Normal file
220
harness-opt/rounds/round-002/module-reach.json
Normal file
@@ -0,0 +1,220 @@
|
||||
{
|
||||
"skills/lark-im/SKILL.md": {
|
||||
"reach": 1.0,
|
||||
"read_cases": [
|
||||
"1",
|
||||
"2",
|
||||
"3"
|
||||
],
|
||||
"actual_cases": [
|
||||
"1",
|
||||
"2",
|
||||
"3"
|
||||
],
|
||||
"expected_cases": [],
|
||||
"discoverability_miss": [],
|
||||
"is_domain_skill": true
|
||||
},
|
||||
"skills/lark-im/references/lark-im-chat-create.md": {
|
||||
"reach": 0.667,
|
||||
"read_cases": [
|
||||
"1",
|
||||
"3"
|
||||
],
|
||||
"actual_cases": [
|
||||
"1",
|
||||
"3"
|
||||
],
|
||||
"expected_cases": [],
|
||||
"discoverability_miss": [],
|
||||
"is_domain_skill": false
|
||||
},
|
||||
"skills/lark-im/references/lark-im-chat-identity.md": {
|
||||
"reach": 0.0,
|
||||
"read_cases": [],
|
||||
"actual_cases": [],
|
||||
"expected_cases": [],
|
||||
"discoverability_miss": [],
|
||||
"is_domain_skill": false
|
||||
},
|
||||
"skills/lark-im/references/lark-im-chat-list.md": {
|
||||
"reach": 0.0,
|
||||
"read_cases": [],
|
||||
"actual_cases": [],
|
||||
"expected_cases": [],
|
||||
"discoverability_miss": [],
|
||||
"is_domain_skill": false
|
||||
},
|
||||
"skills/lark-im/references/lark-im-chat-messages-list.md": {
|
||||
"reach": 0.0,
|
||||
"read_cases": [],
|
||||
"actual_cases": [],
|
||||
"expected_cases": [],
|
||||
"discoverability_miss": [],
|
||||
"is_domain_skill": false
|
||||
},
|
||||
"skills/lark-im/references/lark-im-chat-search.md": {
|
||||
"reach": 0.0,
|
||||
"read_cases": [],
|
||||
"actual_cases": [],
|
||||
"expected_cases": [],
|
||||
"discoverability_miss": [],
|
||||
"is_domain_skill": false
|
||||
},
|
||||
"skills/lark-im/references/lark-im-chat-update.md": {
|
||||
"reach": 0.0,
|
||||
"read_cases": [],
|
||||
"actual_cases": [],
|
||||
"expected_cases": [],
|
||||
"discoverability_miss": [],
|
||||
"is_domain_skill": false
|
||||
},
|
||||
"skills/lark-im/references/lark-im-feed-group-list-item.md": {
|
||||
"reach": 0.0,
|
||||
"read_cases": [],
|
||||
"actual_cases": [],
|
||||
"expected_cases": [],
|
||||
"discoverability_miss": [],
|
||||
"is_domain_skill": false
|
||||
},
|
||||
"skills/lark-im/references/lark-im-feed-group-list.md": {
|
||||
"reach": 0.0,
|
||||
"read_cases": [],
|
||||
"actual_cases": [],
|
||||
"expected_cases": [],
|
||||
"discoverability_miss": [],
|
||||
"is_domain_skill": false
|
||||
},
|
||||
"skills/lark-im/references/lark-im-feed-group-query-item.md": {
|
||||
"reach": 0.0,
|
||||
"read_cases": [],
|
||||
"actual_cases": [],
|
||||
"expected_cases": [],
|
||||
"discoverability_miss": [],
|
||||
"is_domain_skill": false
|
||||
},
|
||||
"skills/lark-im/references/lark-im-feed-groups.md": {
|
||||
"reach": 0.0,
|
||||
"read_cases": [],
|
||||
"actual_cases": [],
|
||||
"expected_cases": [],
|
||||
"discoverability_miss": [],
|
||||
"is_domain_skill": false
|
||||
},
|
||||
"skills/lark-im/references/lark-im-feed-shortcut-create.md": {
|
||||
"reach": 0.0,
|
||||
"read_cases": [],
|
||||
"actual_cases": [],
|
||||
"expected_cases": [],
|
||||
"discoverability_miss": [],
|
||||
"is_domain_skill": false
|
||||
},
|
||||
"skills/lark-im/references/lark-im-feed-shortcut-list.md": {
|
||||
"reach": 0.0,
|
||||
"read_cases": [],
|
||||
"actual_cases": [],
|
||||
"expected_cases": [],
|
||||
"discoverability_miss": [],
|
||||
"is_domain_skill": false
|
||||
},
|
||||
"skills/lark-im/references/lark-im-feed-shortcut-remove.md": {
|
||||
"reach": 0.0,
|
||||
"read_cases": [],
|
||||
"actual_cases": [],
|
||||
"expected_cases": [],
|
||||
"discoverability_miss": [],
|
||||
"is_domain_skill": false
|
||||
},
|
||||
"skills/lark-im/references/lark-im-flag-cancel.md": {
|
||||
"reach": 0.0,
|
||||
"read_cases": [],
|
||||
"actual_cases": [],
|
||||
"expected_cases": [],
|
||||
"discoverability_miss": [],
|
||||
"is_domain_skill": false
|
||||
},
|
||||
"skills/lark-im/references/lark-im-flag-create.md": {
|
||||
"reach": 0.0,
|
||||
"read_cases": [],
|
||||
"actual_cases": [],
|
||||
"expected_cases": [],
|
||||
"discoverability_miss": [],
|
||||
"is_domain_skill": false
|
||||
},
|
||||
"skills/lark-im/references/lark-im-flag-list.md": {
|
||||
"reach": 0.0,
|
||||
"read_cases": [],
|
||||
"actual_cases": [],
|
||||
"expected_cases": [],
|
||||
"discoverability_miss": [],
|
||||
"is_domain_skill": false
|
||||
},
|
||||
"skills/lark-im/references/lark-im-message-enrichment.md": {
|
||||
"reach": 0.0,
|
||||
"read_cases": [],
|
||||
"actual_cases": [],
|
||||
"expected_cases": [],
|
||||
"discoverability_miss": [],
|
||||
"is_domain_skill": false
|
||||
},
|
||||
"skills/lark-im/references/lark-im-messages-mget.md": {
|
||||
"reach": 0.0,
|
||||
"read_cases": [],
|
||||
"actual_cases": [],
|
||||
"expected_cases": [],
|
||||
"discoverability_miss": [],
|
||||
"is_domain_skill": false
|
||||
},
|
||||
"skills/lark-im/references/lark-im-messages-reply.md": {
|
||||
"reach": 0.0,
|
||||
"read_cases": [],
|
||||
"actual_cases": [],
|
||||
"expected_cases": [],
|
||||
"discoverability_miss": [],
|
||||
"is_domain_skill": false
|
||||
},
|
||||
"skills/lark-im/references/lark-im-messages-resources-download.md": {
|
||||
"reach": 0.0,
|
||||
"read_cases": [],
|
||||
"actual_cases": [],
|
||||
"expected_cases": [],
|
||||
"discoverability_miss": [],
|
||||
"is_domain_skill": false
|
||||
},
|
||||
"skills/lark-im/references/lark-im-messages-search.md": {
|
||||
"reach": 0.0,
|
||||
"read_cases": [],
|
||||
"actual_cases": [],
|
||||
"expected_cases": [],
|
||||
"discoverability_miss": [],
|
||||
"is_domain_skill": false
|
||||
},
|
||||
"skills/lark-im/references/lark-im-messages-send.md": {
|
||||
"reach": 0.333,
|
||||
"read_cases": [
|
||||
"3"
|
||||
],
|
||||
"actual_cases": [
|
||||
"3"
|
||||
],
|
||||
"expected_cases": [],
|
||||
"discoverability_miss": [],
|
||||
"is_domain_skill": false
|
||||
},
|
||||
"skills/lark-im/references/lark-im-reactions.md": {
|
||||
"reach": 0.0,
|
||||
"read_cases": [],
|
||||
"actual_cases": [],
|
||||
"expected_cases": [],
|
||||
"discoverability_miss": [],
|
||||
"is_domain_skill": false
|
||||
},
|
||||
"skills/lark-im/references/lark-im-threads-messages-list.md": {
|
||||
"reach": 0.0,
|
||||
"read_cases": [],
|
||||
"actual_cases": [],
|
||||
"expected_cases": [],
|
||||
"discoverability_miss": [],
|
||||
"is_domain_skill": false
|
||||
}
|
||||
}
|
||||
15
harness-opt/rounds/round-002/review.json
Normal file
15
harness-opt/rounds/round-002/review.json
Normal file
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"generated_by": "lark-cli-harness:opt-reviewer",
|
||||
"verdict": "PASS",
|
||||
"module": "skills/lark-im/references/lark-im-messages-send.md",
|
||||
"tier": "T1",
|
||||
"reason": "纯结构性去重:16407→6399 字节(-61%)与策略一致;逐项核对每条承重指令(互斥规则、video-cover 必配、cwd-relative/绝对路径拒绝、markdown→post 边界、三套 <at> 语法、content 各 msg_type 样例、Safety Constraints、identity+scope 映射)均原样保留在新文档内联,删的全是重复/过度罗列(4× 选型规则、镜像 --help 的 Parameters 表、Common Mistakes、Notes、冗余 Commands)。无硬编码评测答案、未针对 080 卡片流窄化、未碰 SKILL.md 身份路由、单文件单根因。",
|
||||
"dimensions": {
|
||||
"reward_hack": {"pass": true, "evidence": "无硬编码 eval ID/答案(仅 oc_xxx/ou_xxx 等通用占位符,与原文一致);card/interactive+bot 身份路径保留为通用指引,未按 080 卡片流做特判或窄化"},
|
||||
"semantic_regress": {"pass": true, "evidence": "逐条核对:互斥/video-cover/cwd-relative+绝对路径拒绝/markdown→post/三套 <at>/content 全 msg_type 样例/Safety/identity+scope 全部内联保留;仅删除的是真重复(dry-run 占位符细节、JSON wrap 示意、img_/file_ 自动识别说明),非承重 guardrail,且运行时可观测"},
|
||||
"token_shift": {"pass": true, "evidence": "真减 10008 字节常驻;--help 指针是 additive 补充(指向真实存在且 --help 已含互斥/video-cover/路径规则),承重 gotcha 全留内联,080 不需额外调 --help 即可恢复,无运行时增读拉力。注:work-order 提的 schema im.messages.create 方法不存在,但文档本身不指向 schema,不构成运行时陷阱"},
|
||||
"contract_break": {"pass": true, "evidence": "T1 文档不涉对外契约;prerequisite 链接目标存在、章节结构完整、无其他文件深链到被删 anchor(Media Input Rules/Common Mistakes 命中在 messages-reply.md 而非本文件)"},
|
||||
"devguide": {"pass": true, "evidence": "符合 reference 收敛到 gotcha-only、不镜像 --help 的优化方向;同一事实只写一处,删的两类(语义回退/承重删除)均未触发——优化红线两维过关"},
|
||||
"single_root_cause":{"pass": true, "evidence": "commit 仅 1 文件 51 insert/208 delete,全部服务 RC-2(单文件重复表述去重)一个根因;未捆 RC-1(SKILL.md)/RC-3(chat-create),未把无关删除以 token 对冲缝入"}
|
||||
}
|
||||
}
|
||||
380
harness-opt/rounds/round-002/round.json
Normal file
380
harness-opt/rounds/round-002/round.json
Normal file
@@ -0,0 +1,380 @@
|
||||
{
|
||||
"round": 2,
|
||||
"status": "admitted",
|
||||
"parent_id": "a1333f2e1f7e98bf6f705814b92cacae1f43565759e4e0c24a0a4700b241649e",
|
||||
"parent_worktree": "/Users/bytedance/Projects/cli",
|
||||
"child_worktree": "/Users/bytedance/Projects/cli",
|
||||
"base_commit": "51f2a70e6dffeea65d928badb6207408490dc215",
|
||||
"module": "skills/lark-im/references/lark-im-messages-send.md",
|
||||
"candidate_modules": [
|
||||
"skills/lark-im/SKILL.md",
|
||||
"skills/lark-im/references/lark-im-chat-create.md",
|
||||
"skills/lark-im/references/lark-im-chat-identity.md",
|
||||
"skills/lark-im/references/lark-im-chat-list.md",
|
||||
"skills/lark-im/references/lark-im-chat-messages-list.md",
|
||||
"skills/lark-im/references/lark-im-chat-search.md",
|
||||
"skills/lark-im/references/lark-im-chat-update.md",
|
||||
"skills/lark-im/references/lark-im-feed-group-list-item.md",
|
||||
"skills/lark-im/references/lark-im-feed-group-list.md",
|
||||
"skills/lark-im/references/lark-im-feed-group-query-item.md",
|
||||
"skills/lark-im/references/lark-im-feed-groups.md",
|
||||
"skills/lark-im/references/lark-im-feed-shortcut-create.md",
|
||||
"skills/lark-im/references/lark-im-feed-shortcut-list.md",
|
||||
"skills/lark-im/references/lark-im-feed-shortcut-remove.md",
|
||||
"skills/lark-im/references/lark-im-flag-cancel.md",
|
||||
"skills/lark-im/references/lark-im-flag-create.md",
|
||||
"skills/lark-im/references/lark-im-flag-list.md",
|
||||
"skills/lark-im/references/lark-im-message-enrichment.md",
|
||||
"skills/lark-im/references/lark-im-messages-mget.md",
|
||||
"skills/lark-im/references/lark-im-messages-reply.md",
|
||||
"skills/lark-im/references/lark-im-messages-resources-download.md",
|
||||
"skills/lark-im/references/lark-im-messages-search.md",
|
||||
"skills/lark-im/references/lark-im-messages-send.md",
|
||||
"skills/lark-im/references/lark-im-reactions.md",
|
||||
"skills/lark-im/references/lark-im-threads-messages-list.md"
|
||||
],
|
||||
"module_reach": {
|
||||
"skills/lark-im/SKILL.md": {
|
||||
"reach": 1.0,
|
||||
"read_cases": [
|
||||
"1",
|
||||
"2",
|
||||
"3"
|
||||
],
|
||||
"actual_cases": [
|
||||
"1",
|
||||
"2",
|
||||
"3"
|
||||
],
|
||||
"expected_cases": [],
|
||||
"discoverability_miss": [],
|
||||
"is_domain_skill": true
|
||||
},
|
||||
"skills/lark-im/references/lark-im-chat-create.md": {
|
||||
"reach": 0.667,
|
||||
"read_cases": [
|
||||
"1",
|
||||
"3"
|
||||
],
|
||||
"actual_cases": [
|
||||
"1",
|
||||
"3"
|
||||
],
|
||||
"expected_cases": [],
|
||||
"discoverability_miss": [],
|
||||
"is_domain_skill": false
|
||||
},
|
||||
"skills/lark-im/references/lark-im-chat-identity.md": {
|
||||
"reach": 0.0,
|
||||
"read_cases": [],
|
||||
"actual_cases": [],
|
||||
"expected_cases": [],
|
||||
"discoverability_miss": [],
|
||||
"is_domain_skill": false
|
||||
},
|
||||
"skills/lark-im/references/lark-im-chat-list.md": {
|
||||
"reach": 0.0,
|
||||
"read_cases": [],
|
||||
"actual_cases": [],
|
||||
"expected_cases": [],
|
||||
"discoverability_miss": [],
|
||||
"is_domain_skill": false
|
||||
},
|
||||
"skills/lark-im/references/lark-im-chat-messages-list.md": {
|
||||
"reach": 0.0,
|
||||
"read_cases": [],
|
||||
"actual_cases": [],
|
||||
"expected_cases": [],
|
||||
"discoverability_miss": [],
|
||||
"is_domain_skill": false
|
||||
},
|
||||
"skills/lark-im/references/lark-im-chat-search.md": {
|
||||
"reach": 0.0,
|
||||
"read_cases": [],
|
||||
"actual_cases": [],
|
||||
"expected_cases": [],
|
||||
"discoverability_miss": [],
|
||||
"is_domain_skill": false
|
||||
},
|
||||
"skills/lark-im/references/lark-im-chat-update.md": {
|
||||
"reach": 0.0,
|
||||
"read_cases": [],
|
||||
"actual_cases": [],
|
||||
"expected_cases": [],
|
||||
"discoverability_miss": [],
|
||||
"is_domain_skill": false
|
||||
},
|
||||
"skills/lark-im/references/lark-im-feed-group-list-item.md": {
|
||||
"reach": 0.0,
|
||||
"read_cases": [],
|
||||
"actual_cases": [],
|
||||
"expected_cases": [],
|
||||
"discoverability_miss": [],
|
||||
"is_domain_skill": false
|
||||
},
|
||||
"skills/lark-im/references/lark-im-feed-group-list.md": {
|
||||
"reach": 0.0,
|
||||
"read_cases": [],
|
||||
"actual_cases": [],
|
||||
"expected_cases": [],
|
||||
"discoverability_miss": [],
|
||||
"is_domain_skill": false
|
||||
},
|
||||
"skills/lark-im/references/lark-im-feed-group-query-item.md": {
|
||||
"reach": 0.0,
|
||||
"read_cases": [],
|
||||
"actual_cases": [],
|
||||
"expected_cases": [],
|
||||
"discoverability_miss": [],
|
||||
"is_domain_skill": false
|
||||
},
|
||||
"skills/lark-im/references/lark-im-feed-groups.md": {
|
||||
"reach": 0.0,
|
||||
"read_cases": [],
|
||||
"actual_cases": [],
|
||||
"expected_cases": [],
|
||||
"discoverability_miss": [],
|
||||
"is_domain_skill": false
|
||||
},
|
||||
"skills/lark-im/references/lark-im-feed-shortcut-create.md": {
|
||||
"reach": 0.0,
|
||||
"read_cases": [],
|
||||
"actual_cases": [],
|
||||
"expected_cases": [],
|
||||
"discoverability_miss": [],
|
||||
"is_domain_skill": false
|
||||
},
|
||||
"skills/lark-im/references/lark-im-feed-shortcut-list.md": {
|
||||
"reach": 0.0,
|
||||
"read_cases": [],
|
||||
"actual_cases": [],
|
||||
"expected_cases": [],
|
||||
"discoverability_miss": [],
|
||||
"is_domain_skill": false
|
||||
},
|
||||
"skills/lark-im/references/lark-im-feed-shortcut-remove.md": {
|
||||
"reach": 0.0,
|
||||
"read_cases": [],
|
||||
"actual_cases": [],
|
||||
"expected_cases": [],
|
||||
"discoverability_miss": [],
|
||||
"is_domain_skill": false
|
||||
},
|
||||
"skills/lark-im/references/lark-im-flag-cancel.md": {
|
||||
"reach": 0.0,
|
||||
"read_cases": [],
|
||||
"actual_cases": [],
|
||||
"expected_cases": [],
|
||||
"discoverability_miss": [],
|
||||
"is_domain_skill": false
|
||||
},
|
||||
"skills/lark-im/references/lark-im-flag-create.md": {
|
||||
"reach": 0.0,
|
||||
"read_cases": [],
|
||||
"actual_cases": [],
|
||||
"expected_cases": [],
|
||||
"discoverability_miss": [],
|
||||
"is_domain_skill": false
|
||||
},
|
||||
"skills/lark-im/references/lark-im-flag-list.md": {
|
||||
"reach": 0.0,
|
||||
"read_cases": [],
|
||||
"actual_cases": [],
|
||||
"expected_cases": [],
|
||||
"discoverability_miss": [],
|
||||
"is_domain_skill": false
|
||||
},
|
||||
"skills/lark-im/references/lark-im-message-enrichment.md": {
|
||||
"reach": 0.0,
|
||||
"read_cases": [],
|
||||
"actual_cases": [],
|
||||
"expected_cases": [],
|
||||
"discoverability_miss": [],
|
||||
"is_domain_skill": false
|
||||
},
|
||||
"skills/lark-im/references/lark-im-messages-mget.md": {
|
||||
"reach": 0.0,
|
||||
"read_cases": [],
|
||||
"actual_cases": [],
|
||||
"expected_cases": [],
|
||||
"discoverability_miss": [],
|
||||
"is_domain_skill": false
|
||||
},
|
||||
"skills/lark-im/references/lark-im-messages-reply.md": {
|
||||
"reach": 0.0,
|
||||
"read_cases": [],
|
||||
"actual_cases": [],
|
||||
"expected_cases": [],
|
||||
"discoverability_miss": [],
|
||||
"is_domain_skill": false
|
||||
},
|
||||
"skills/lark-im/references/lark-im-messages-resources-download.md": {
|
||||
"reach": 0.0,
|
||||
"read_cases": [],
|
||||
"actual_cases": [],
|
||||
"expected_cases": [],
|
||||
"discoverability_miss": [],
|
||||
"is_domain_skill": false
|
||||
},
|
||||
"skills/lark-im/references/lark-im-messages-search.md": {
|
||||
"reach": 0.0,
|
||||
"read_cases": [],
|
||||
"actual_cases": [],
|
||||
"expected_cases": [],
|
||||
"discoverability_miss": [],
|
||||
"is_domain_skill": false
|
||||
},
|
||||
"skills/lark-im/references/lark-im-messages-send.md": {
|
||||
"reach": 0.333,
|
||||
"read_cases": [
|
||||
"3"
|
||||
],
|
||||
"actual_cases": [
|
||||
"3"
|
||||
],
|
||||
"expected_cases": [],
|
||||
"discoverability_miss": [],
|
||||
"is_domain_skill": false
|
||||
},
|
||||
"skills/lark-im/references/lark-im-reactions.md": {
|
||||
"reach": 0.0,
|
||||
"read_cases": [],
|
||||
"actual_cases": [],
|
||||
"expected_cases": [],
|
||||
"discoverability_miss": [],
|
||||
"is_domain_skill": false
|
||||
},
|
||||
"skills/lark-im/references/lark-im-threads-messages-list.md": {
|
||||
"reach": 0.0,
|
||||
"read_cases": [],
|
||||
"actual_cases": [],
|
||||
"expected_cases": [],
|
||||
"discoverability_miss": [],
|
||||
"is_domain_skill": false
|
||||
}
|
||||
},
|
||||
"expected_reach": {},
|
||||
"minibatch": [
|
||||
"1",
|
||||
"2",
|
||||
"3"
|
||||
],
|
||||
"pareto_cases": [
|
||||
"1",
|
||||
"2",
|
||||
"3"
|
||||
],
|
||||
"artifacts": {
|
||||
"workorder": "workorder.md",
|
||||
"diagnosis": "diagnosis.md",
|
||||
"attribution": "attribution.json",
|
||||
"strategy": "strategy.md",
|
||||
"review": "review.json",
|
||||
"trend": "trend.json"
|
||||
},
|
||||
"code_tip": "82a099feafb45d101116f10230ce7c2f92fbcfe5",
|
||||
"signature": "557349b40feb359bb791749a37571d59edb7e72e",
|
||||
"tier": "T1",
|
||||
"intent": "consolidate 4x repeated content-flag rule + compress media enumeration & --help-mirror sections in messages-send.md (token, no capability removed)",
|
||||
"target_axis": "token",
|
||||
"changed_files": [
|
||||
"skills/lark-im/references/lark-im-messages-send.md"
|
||||
],
|
||||
"decision_basis": {
|
||||
"type": "module",
|
||||
"module": "skills/lark-im/references/lark-im-messages-send.md"
|
||||
},
|
||||
"decision_cases": [
|
||||
"3"
|
||||
],
|
||||
"review": {
|
||||
"generated_by": "lark-cli-harness:opt-reviewer",
|
||||
"verdict": "PASS",
|
||||
"module": "skills/lark-im/references/lark-im-messages-send.md",
|
||||
"tier": "T1",
|
||||
"reason": "纯结构性去重:16407→6399 字节(-61%)与策略一致;逐项核对每条承重指令(互斥规则、video-cover 必配、cwd-relative/绝对路径拒绝、markdown→post 边界、三套 <at> 语法、content 各 msg_type 样例、Safety Constraints、identity+scope 映射)均原样保留在新文档内联,删的全是重复/过度罗列(4× 选型规则、镜像 --help 的 Parameters 表、Common Mistakes、Notes、冗余 Commands)。无硬编码评测答案、未针对 080 卡片流窄化、未碰 SKILL.md 身份路由、单文件单根因。",
|
||||
"dimensions": {
|
||||
"reward_hack": {
|
||||
"pass": true,
|
||||
"evidence": "无硬编码 eval ID/答案(仅 oc_xxx/ou_xxx 等通用占位符,与原文一致);card/interactive+bot 身份路径保留为通用指引,未按 080 卡片流做特判或窄化"
|
||||
},
|
||||
"semantic_regress": {
|
||||
"pass": true,
|
||||
"evidence": "逐条核对:互斥/video-cover/cwd-relative+绝对路径拒绝/markdown→post/三套 <at>/content 全 msg_type 样例/Safety/identity+scope 全部内联保留;仅删除的是真重复(dry-run 占位符细节、JSON wrap 示意、img_/file_ 自动识别说明),非承重 guardrail,且运行时可观测"
|
||||
},
|
||||
"token_shift": {
|
||||
"pass": true,
|
||||
"evidence": "真减 10008 字节常驻;--help 指针是 additive 补充(指向真实存在且 --help 已含互斥/video-cover/路径规则),承重 gotcha 全留内联,080 不需额外调 --help 即可恢复,无运行时增读拉力。注:work-order 提的 schema im.messages.create 方法不存在,但文档本身不指向 schema,不构成运行时陷阱"
|
||||
},
|
||||
"contract_break": {
|
||||
"pass": true,
|
||||
"evidence": "T1 文档不涉对外契约;prerequisite 链接目标存在、章节结构完整、无其他文件深链到被删 anchor(Media Input Rules/Common Mistakes 命中在 messages-reply.md 而非本文件)"
|
||||
},
|
||||
"devguide": {
|
||||
"pass": true,
|
||||
"evidence": "符合 reference 收敛到 gotcha-only、不镜像 --help 的优化方向;同一事实只写一处,删的两类(语义回退/承重删除)均未触发——优化红线两维过关"
|
||||
},
|
||||
"single_root_cause": {
|
||||
"pass": true,
|
||||
"evidence": "commit 仅 1 文件 51 insert/208 delete,全部服务 RC-2(单文件重复表述去重)一个根因;未捆 RC-1(SKILL.md)/RC-3(chat-create),未把无关删除以 token 对冲缝入"
|
||||
}
|
||||
}
|
||||
},
|
||||
"child_k": 5,
|
||||
"eval_trace": null,
|
||||
"retro": {
|
||||
"cause": "已入池",
|
||||
"noise_borderline": false,
|
||||
"summary": "越带入池,无需复盘补发"
|
||||
},
|
||||
"retro_sessions": [
|
||||
{
|
||||
"case": "3",
|
||||
"session": null,
|
||||
"axis": "token",
|
||||
"expect": "降",
|
||||
"parent": 37942,
|
||||
"child": 35478,
|
||||
"gain": "收益现",
|
||||
"pass_delta": null
|
||||
}
|
||||
],
|
||||
"verdict": "admitted",
|
||||
"ci": null,
|
||||
"new_candidate": "557349b40feb359bb791749a37571d59edb7e72e",
|
||||
"decision": {
|
||||
"parent_success": 1.0,
|
||||
"child_success": 1.0,
|
||||
"parent_score": 0.6,
|
||||
"child_score": 1.0,
|
||||
"score_saved": 0.4,
|
||||
"score_threshold": 0.09532271373123208,
|
||||
"parent_token": 37942.0,
|
||||
"child_token": 35478.0,
|
||||
"saved": 2464.0,
|
||||
"threshold": 4532.708313776408,
|
||||
"parent_duration": 45769.0,
|
||||
"child_duration": 46540.0,
|
||||
"dur_saved": -771.0,
|
||||
"dur_threshold": 4899.200953624988,
|
||||
"dur_margin": 1.0,
|
||||
"missing_duration": [],
|
||||
"k_child": 5,
|
||||
"k_parent": 5,
|
||||
"decision_n": 1,
|
||||
"missing_context": [],
|
||||
"missing_score": [],
|
||||
"parent_token_acc": 251669.0,
|
||||
"child_token_acc": 221685.0,
|
||||
"phi0_score": 0.5333333333333333,
|
||||
"eff_margin": 1.0,
|
||||
"parent_token_full": 37942.0,
|
||||
"child_token_full": 35478.0,
|
||||
"saved_full": 2464.0,
|
||||
"observe_n": 1,
|
||||
"target_axis": "token",
|
||||
"admitted": true,
|
||||
"reason": "score_gain"
|
||||
},
|
||||
"patch": "verify_results/round-002-lark-im-references-lark-im-messages-send.patch"
|
||||
}
|
||||
48
harness-opt/rounds/round-002/strategy.md
Normal file
48
harness-opt/rounds/round-002/strategy.md
Normal file
@@ -0,0 +1,48 @@
|
||||
# Round 2 候选策略(模块=references/lark-im-messages-send.md, tier=T1, 主指标=token)
|
||||
|
||||
## 根因与选择
|
||||
|
||||
| 根因 | 来源(评测归因/规范经验) | 承载模块(reach) | annotation 风险级 | coverage 档 | P级 | 选中 |
|
||||
|---|---|---|---|---|---|---|
|
||||
| RC-2: messages-send.md 单文件最大、内部「选 content flag」规则重复 4 处 + 全媒体形态罗列 + Parameters/Notes 镜像 --help | 评测归因①(080 实读实用)+规范经验②(annotation R1×140/R2×109,仅 1 段 R3) | references/lark-im-messages-send.md (0.333) | R1/R2 主导,唯一 R3=Safety Constraints(L9–22) | 密 / overfit 低 | P1 | ✅ |
|
||||
| RC-1: SKILL.md `## Important Notes` 低命中 + `## Shortcuts` 全表常驻 | 评测归因①(reach=1.0,3 题全命中) | SKILL.md (1.0) | R2/R3 混合(identity/约束密集) | 密 / 中 | P0(命中) 但 effect 高风险 | |
|
||||
| RC-3: chat-create.md 按需偏大 | 评测归因① | references/lark-im-chat-create.md (0.667) | — | 密 | P1 | |
|
||||
|
||||
- **选中理由**:RC-2 是诊断点名「最干净的 token 杠杆」——单文件最大块(实测 ~5,365 tok,占 080 visible 24.8%),且 080 调用前已读、确实据它发卡片成功(reach=0.333、actual=1,非「读了没用」)。annotation 证实它 R1/R2 主导(140 R1 + 109 R2 行,可重构/可压缩),唯一 R3 段是 Safety Constraints(L9–22),我**原样保留语义**。coverage=「密」、overfit「低」→ 本轮 eval 能在 080 上裁真伪。这是纯减体积、零能力删除、不碰 SKILL.md 路由的改动。
|
||||
- **为什么不选 RC-1**:reach=1.0、命中率最高,但 diagnosis 明确标它为 **effect 风险点**——剩余内容多为 identity/约束类,正是驱动 015/080 走通 bot 身份判断的承重内容;objective 的**硬门槛是「保住成功率」**,动 SKILL.md 最可能误伤这条已绿链路。本轮放弃,避免拿成功率换 token。
|
||||
- **为什么不选 RC-3**:diagnosis 判其杠杆最低(体积不离群),列为更次级;同一根因一轮只动一个,留待后续轮次。
|
||||
- **选模块理由**:messages-send.md reach=0.333>0(满足 reach 锁),承载选中的 RC-2,是非域 reference、改它不触碰 SKILL.md 的身份路由面。多文件无——本轮只动这一个文件。
|
||||
- **规范经验源补注**:对照 content-taxonomy——「单命令用法/长示例/与 --help 重复」类默认 R0/R1,「一般行为规则/CLI 机制约定」默认 R2;本文件的重复选型规则、全形态 Commands、Parameters/Notes 镜像即此类,处理方向为「留命中率最高一处,其余删或指针」「高频留 2–3 例,长的下沉」。当轮可被 080 裁真伪(coverage 密/overfit 低)。
|
||||
|
||||
## 改了什么(逐处)
|
||||
- **L23–43 `## Choose The Right Content Flag` + `### --text vs --markdown`**:两段语义重叠的选型说明 → 合并为单张 4 行选型表(markdown/text/content/media),并把互斥规则并入表后一句。删掉 `### --text vs --markdown` 整段(与表重复)。
|
||||
- **L44–82 `## What --markdown Really Does` + `### Markdown Boundaries` + `### Image Constraint`**:三段约 39 行 → 压成 `## --markdown Gotchas` 三条要点(强制 post/无 title、标题改写规则、图片预上传 vs 远程 URL vs 本地路径不支持)。删掉 JSON wrap 示意、逐条 boundary 罗列等可由行为观察得到的展开。
|
||||
- **L83–93 图片预上传双命令示例**:并入 `## Commands` 的一条 markdown+image 示例(保留 `im images create` → 引用 img_xxx 的关键两步)。
|
||||
- **L114–161 `## Commands`(15+ 例覆盖全媒体形态)+ `## Media Input Rules`**:压成代表性示例(markdown / text / DM / post-title / markdown+image / 4 个媒体一组 / idempotency+dry-run),媒体路径规则收成 `--help` 指针后的 3 条 load-bearing gotcha(cwd-relative/绝对路径拒绝、video-cover 必配、msg-type 推断冲突)。
|
||||
- **L169–191 `## Parameters` 表**:删除镜像 `--help` 的逐参数描述,改为「Run `lark-cli im +messages-send --help`」指针 + 仅保留 --help 不显然的三条硬规则(已并入 Commands 末尾)。
|
||||
- **L192–202 `## Common Mistakes`**:整段删除——逐条都是选型表/markdown gotcha 的反向重述(第 4 次重复选型规则),删后选型信息仍在表里。
|
||||
- **L203–216 `## content Format Reference`**:保留(构造 `--content` 的 gotcha),把 image/file/audio 三行合并为一行省重复。
|
||||
- **L227–248 `## @Mention Format`**:保留全部三种 msg_type 的 `<at>` 语法(text/post/interactive 各异、AI 猜不到),压紧为两条要点、去掉小标题与重复散文。
|
||||
- **L249–264 `## Notes`**:整段删除——逐条(互斥/media 上传/scope/markdown 强制 post/video-cover/msg-type 冲突)均已在 Safety Constraints、选型表、--markdown Gotchas、Commands 指针处各保留一处单一事实源。
|
||||
|
||||
## 为什么这么改(机制)
|
||||
- **消除根因的因果链**:该 reference 的体积来自「同一份选型规则在 4 个 section 重复 + 全媒体形态逐条罗列 + Parameters/Notes 镜像 --help」。token 不是被任务必需信息占用,而是被**重复表述**占用。按「同一份事实只写一次」(锚点 1)合并到单一事实源后,每条 load-bearing 信息仍恰好出现一次,080 这类「读该 reference→发消息」的题,读入 token 直接下降而行为不变。
|
||||
- **不删能力**:每个 flag(text/markdown/content/image/file/video/audio/idempotency/dry-run/msg-type/video-cover/as)、每条硬约束(互斥、video-cover 必配、cwd-relative 路径、绝对路径拒绝、markdown 强制 post/无 title、msg-type 冲突校验)、三套 `<at>` 语法、content 各 msg_type 样例、Safety Constraints、identity+scope 映射——全部保留,只是从「重复 N 次/逐条罗列」变成「一处/代表性示例 + --help 指针」。
|
||||
- **规范经验源**:依 optimization-playbook「reference 收敛到 gotcha-only,不做 --help 镜像」——Parameters 全表/全形态 Commands 属 USAGE,下沉到 `--help` 指针;保留的是 --help 表达不了的跨 flag 互斥、媒体路径安全、markdown→post 边界、@mention 按类型差异等 gotcha。annotation 标这些段为 R1(可重构/下沉),符合处理方向;唯一 R3(Safety)原样保留。
|
||||
|
||||
## 预期效果
|
||||
- **成功率**:不退化。080(唯一读该文件的题)的发卡片链路依赖的是 `--content`/`interactive`、identity=bot、chat-id——全部保留;选型表、content Format Reference、Safety、scope 都在。015/080 走通 bot 身份的判断由 SKILL.md + identity 段承载,本轮**没碰 SKILL.md**,零误伤面。014 与本文件无关(reach 不含 014)。
|
||||
- **context(分两层)**:
|
||||
- (1) **静态字数差**:16,407 → 6,399 chars(-61.0%);tiktoken cl100k 3,869 → 1,799 tok(-53.5%)。(注:diagnosis 报 ~5,365 tok 系另一 tokenizer/含注入开销;此处用 cl100k 自测,方向与幅度一致。)
|
||||
- (2) **运行时 context 方向**:仅在**实读该 reference 的子集**生效——本轮即 080 一题,运行时读入下降约 50%+(该块占 080 visible 24.8%,预计 080 visible 降约 12–13%)。其余两题(014/015)不读该文件,运行时 token **不变**(既不增也不减)。这是按需 reference,不是常驻面,不会影响未读它的题。
|
||||
- **覆盖敞口**:RC-2 子集仅 080 一题(reach=0.333),证据基数小。coverage 判该文件「密/overfit 低」,本轮 eval 可在 080 上裁真伪,但单题不可外推到「所有发消息任务」。建议后续补「读 messages-send.md 后用 --markdown / 媒体 / @mention」的 case 加厚子集。预期收益落在 **token 轴**(080 visible 下降),effect 轴维持不退化。
|
||||
|
||||
## 刻意没做什么(反 reward-hack / 反过拟合)
|
||||
- 没硬编码任何评测题答案;没删任何能力、flag、guardrail、身份/scope 说明;没碰 lark-im 以外文件,也没把无关根因捆进本轮(commit 仅 1 个文件)。
|
||||
- **没碰 SKILL.md(RC-1)**:尽管 reach=1.0 杠杆最大,但其剩余内容是驱动 015/080 bot 身份判断的承重 identity/约束,diagnosis 标为 effect 风险点;在「保住成功率」硬门槛下不拿成功率换 token。
|
||||
- **没补收窄/分页指引**(015 的 22.5k chat-messages-list 黑洞):那是「增内容」,与降 token 目标方向相反,diagnosis 已列为观察项、本轮不做。
|
||||
- 本改动**不是按评测错误反推**的参数/路由拟合——是基于 annotation + content-taxonomy 的结构性去重,删的是重复表述而非按 080 的具体内容裁剪;真实价值在「任何读该 reference 的发消息任务都少读重复 token」,080 只是当轮可验证的子集。
|
||||
- 未发现需要 breaking(T3)才能根治的点;本轮纯 T1 文档去重即可。
|
||||
|
||||
## 签名
|
||||
- signature: 557349b40feb359bb791749a37571d59edb7e72e (commit 82a099fe 的 diff hash) tier: T1
|
||||
11
harness-opt/rounds/round-002/trend.json
Normal file
11
harness-opt/rounds/round-002/trend.json
Normal file
@@ -0,0 +1,11 @@
|
||||
[
|
||||
{
|
||||
"round": 1,
|
||||
"n": 3,
|
||||
"pass_n": 0,
|
||||
"cmd_fail_rate": 0.6,
|
||||
"tool_calls": 26.333333333333332,
|
||||
"duration_ms": 50189.0,
|
||||
"token": 31997.0
|
||||
}
|
||||
]
|
||||
43
harness-opt/rounds/round-002/workorder.md
Normal file
43
harness-opt/rounds/round-002/workorder.md
Normal file
@@ -0,0 +1,43 @@
|
||||
# Round 2 归因派工单(parent=a1333f2e1f7e98bf6f705814b92cacae1f43565759e4e0c24a0a4700b241649e;模块未定,由 candidate-writer 据诊断点名)
|
||||
|
||||
> **只读输入**——opt-attributor 读本文件,把诊断**另写** `diagnosis.md`(给 candidate-writer)+ 逐题结构化 `attribution.json`(给 dashboard)。**不要覆盖本文件**,留作派工单↔诊断的前后对比。
|
||||
> 判分点只当「什么算挂」的锚,禁止照抄 grader 药方(已从派工单剔除)。
|
||||
|
||||
## 模块运行时可达性(选模块第一步的证据;要选须在 strategy.md 说明理由)
|
||||
> reach=**实测**触达率(域主 SKILL.md 经 Skill 工具加载、reference 经 Read,都从 trace 实测,没有恒在的面);判决集=实测∪预期触达。**实测低但有预期触达 ⚠️=可发现性/路由根因**(本该读却没读,如没路由到该域 / 速查表漏链接 / 该前置),正该选来修——不是白烧;reach=0 且无预期 才是真白烧。 **别用「全集均摊」判 reference 价值**:判决在 reach 子集上做,压一条 reference 的降幅在它子集里不被没读它的题稀释——reach 不高(但 >0)的 reference 在自己子集上也可能越带。
|
||||
- `skills/lark-im/SKILL.md` → reach=1.0 [域主 skill·经 Skill 工具加载];判决集(实测∪预期): ['1', '2', '3']
|
||||
- `skills/lark-im/references/lark-im-chat-create.md` → reach=0.667;判决集(实测∪预期): ['1', '3']
|
||||
- `skills/lark-im/references/lark-im-messages-send.md` → reach=0.333;判决集(实测∪预期): ['3']
|
||||
- (另 22 个 reference reach=0 且无预期触达,本轮无关,略)
|
||||
|
||||
## 逐轮诊断信号趋势(纯诊断,不进判决)
|
||||
|
||||
| 轮 | 题数 | PASS | 命令失败率 | 工具调用 | 耗时(ms) | token |
|
||||
|---|---|---|---|---|---|---|
|
||||
| R1 | 3 | 0 | 0.60 | 26 | 50189 | 31997 |
|
||||
|
||||
> 跨题均值,按轮排。**命令失败率、工具调用数是横切诊断信号,不是准入轴**(准入只走 效果/token/耗时)——用来判「上一轮那刀有没有把失败/轮次压下去」。工具调用数比 wall-clock 稳,可给噪声大的耗时轴当旁证。
|
||||
|
||||
### 1 [PASS] ctx=34270 (acc=274608) 43995ms tools=31
|
||||
- session.jsonl: harness-opt/rounds/round-001/child-runs/run-1/detail_info/cases/CLI_核心评测_014/0/session.jsonl [native]
|
||||
- 判分点(grader 的「什么算挂」oracle,非药方):
|
||||
✗ 使用当前用户身份创建名为「IM合作群」的群聊
|
||||
证据: transcript 在展示授权二维码后结束,无任何 `lark-cli im +chat-create` 调用。执行停在 '授权完成后请告诉我,我会继续帮你创建群聊并发送消息',群聊未创建。
|
||||
✗ 将傅一铭和傅二铭加入该群
|
||||
证据: transcript 显示尝试搜索用户时遇到 `need_user_authorization` 错误,授权流程启动后中断。未获取到任何用户的 open_id,无后续添加操作。
|
||||
✗ 在该群发送文本消息「大家体验有问题随时沟通」,并返回可验证的 chat_id / message_id
|
||||
证据: 群聊未创建,无 chat_id 可返回。transcript 无任何 `lark-cli im messages-send` 调用。
|
||||
|
||||
### 2 [PASS] ctx=47116 (acc=612048) 114310ms tools=49
|
||||
- session.jsonl: harness-opt/rounds/round-001/child-runs/run-1/detail_info/cases/CLI_核心评测_015/0/session.jsonl [native]
|
||||
- 判分点(grader 的「什么算挂」oracle,非药方):
|
||||
✓ 成功定位名为「fusanming_at_openclaw群」的群,并获取最近包含「飞豆」关键字的消息
|
||||
✓ 将筛选出的相关消息内容转发到「fusanming_at_需求测试群」
|
||||
✓ 在「fusanming_at_需求测试群」中 @傅六铭 做知会,消息发送成功
|
||||
|
||||
### 3 [PASS] ctx=37942 (acc=251669) 45769ms tools=23
|
||||
- session.jsonl: harness-opt/rounds/round-001/child-runs/run-1/detail_info/cases/CLI_核心评测_080/0/session.jsonl [native]
|
||||
- 判分点(grader 的「什么算挂」oracle,非药方):
|
||||
✓ 使用用户身份创建一个名为「今晚吃什么」的群,预期返回 chat_id
|
||||
✓ 创建一张飞书卡片,卡片内容包含「今天晚上吃什么」
|
||||
✓ 将该卡片发送到新建群中,预期返回 message_id
|
||||
59
harness-opt/rounds/round-003/attribution.json
Normal file
59
harness-opt/rounds/round-003/attribution.json
Normal file
@@ -0,0 +1,59 @@
|
||||
[
|
||||
{
|
||||
"case_id": "1",
|
||||
"verdict": "PASS",
|
||||
"verdict_note": "workorder=PASS(聚合口径);判分点证据 3/3 ✗,按判分点当实质 FAIL 处理",
|
||||
"token": 34555,
|
||||
"duration_ms": 37000,
|
||||
"tool_calls": 31,
|
||||
"cmd_attempts": 7,
|
||||
"cmd_failures": 5,
|
||||
"cmd_fail_rate": 0.71,
|
||||
"discoverability_state": "无(失败命令全是跨域 contact + auth,非 lark-im;chat-create.md 调用前已读但未走到使用)",
|
||||
"axis": "效果",
|
||||
"root_cause": "沙箱 user 授权不可完成 + 跨域 lark-contact 命令依赖;无 lark-im 文档根因,本轮不改",
|
||||
"token_hotspot": "运行时冗余清单常驻(SKILL.md 3,456)+ 按需 chat-create.md 3,062(读了没用上);lark-shared 3,751 与系统 Skill 列表注入 4,612 均不归因",
|
||||
"token_reliability": "常驻静态(SKILL.md)/ 按需读取(chat-create.md,本题读了没用上)",
|
||||
"duration_hotspot": "多轮交互(查联系人→切 contact→失败→授权→qrcode 重试)+ 纯外部API延迟(部分不可归因)",
|
||||
"duration_reliability": "耗时波动大,单次运行不算数,需多题或多次复现",
|
||||
"doc_fix_hint": "效果侧无 lark-im 文档缺信息(环境+跨域);token 侧 chat-create.md 把同组 flag 在 Commands/Usage Scenarios 重复演示、Common Errors 复述 validation 字符串,属可删冗余"
|
||||
},
|
||||
{
|
||||
"case_id": "2",
|
||||
"verdict": "PASS",
|
||||
"verdict_note": "真 PASS,判分点 3/3 ✓,全程 bot 身份无授权阻断",
|
||||
"token": 54568,
|
||||
"duration_ms": 125000,
|
||||
"tool_calls": 49,
|
||||
"cmd_attempts": 11,
|
||||
"cmd_failures": 3,
|
||||
"cmd_fail_rate": 0.27,
|
||||
"discoverability_state": "① 从没读(messages-search.md / chat-messages-list.md reach=0,直接猜命令;本题未读任何 lark-im reference)",
|
||||
"axis": "token",
|
||||
"root_cause": "无过滤 +chat-messages-list --page-all 全量拉取 → 43.5KB 输出被 Read 整文件灌入 22,556 tok;token 大头非 lark-im doc。修它需补收窄/前置内容,与降 token 目标方向冲突,列观察项",
|
||||
"token_hotspot": "工具返回原样输出(block #19 单次 Read 22,556 tok / 51.5%,归「其他工具调用/返回」)",
|
||||
"token_reliability": "单次输出(强依赖该群消息量,单题不可外推,非稳定常驻热点)",
|
||||
"duration_hotspot": "多轮交互 + 重试(messages-search 连环 exit2 → page-all → 大输出 → 多次本地 grep 抠数据)",
|
||||
"duration_reliability": "耗时波动大,单次运行不算数,需多题或多次复现(model calls 16 作旁证,明显高于 080)",
|
||||
"doc_fix_hint": "本题无 T1 可发力的 token 抓手(大头是单次工具输出,非 lark-im doc 常驻);缺的是大群消息查询的 server-side 收窄指引,但补它=增内容、与降 token 反向,不作本轮根因"
|
||||
},
|
||||
{
|
||||
"case_id": "3",
|
||||
"verdict": "PASS",
|
||||
"verdict_note": "真 PASS,判分点 3/3 ✓,主动选 bot 身份建群+发卡片均 ok:true,零命令失败",
|
||||
"token": 38009,
|
||||
"duration_ms": 47000,
|
||||
"tool_calls": 22,
|
||||
"cmd_attempts": 3,
|
||||
"cmd_failures": 0,
|
||||
"cmd_fail_rate": 0.0,
|
||||
"discoverability_state": "无(无失败命令;SKILL.md + chat-create.md + messages-send.md 全部状态③:调用前已读且用上)",
|
||||
"axis": "token",
|
||||
"root_cause": "读取 Skill 占 56.4%;本轮唯一干净 token 抓手 = chat-create.md 内部冗余(示例罗列 + 场景重复 + --help 镜像),从未被优化过",
|
||||
"token_hotspot": "运行时冗余清单常驻 + 按需 reference(chat-create.md 当前 2,336 raw,可压 Commands/Usage Scenarios 重叠 + Common Errors validation 镜像;trace 里 messages-send.md 5,365 是旧版,round-2 已压到 2,006,本轮不再可压)",
|
||||
"token_reliability": "按需读取(chat-create.md reach=0.667,本题是其压缩收益唯一稳态兑现题)",
|
||||
"duration_hotspot": "无离群(建群+发卡片正常串行,无重试、无写后回查)",
|
||||
"duration_reliability": "耗时波动大,单次运行不算数,需多题或多次复现",
|
||||
"doc_fix_hint": "chat-create.md 把同组 flag 在 Commands(12 例)+Usage Scenarios(3 场景)重复演示、Common Errors 多行复述 --help/报错本身就会吐的 validation 字符串,属可删冗余;232043 两步流 / --chat-mode topic 区分 / --owner 默认为载重红线,压缩中不可误删"
|
||||
}
|
||||
]
|
||||
27
harness-opt/rounds/round-003/case-commands.json
Normal file
27
harness-opt/rounds/round-003/case-commands.json
Normal file
@@ -0,0 +1,27 @@
|
||||
{
|
||||
"1": [
|
||||
"auth login",
|
||||
"auth qrcode",
|
||||
"contact +search-user"
|
||||
],
|
||||
"3": [
|
||||
"auth login",
|
||||
"auth qrcode",
|
||||
"auth status",
|
||||
"im +chat-create",
|
||||
"im +messages-send"
|
||||
],
|
||||
"2": [
|
||||
"auth login",
|
||||
"auth qrcode",
|
||||
"auth status",
|
||||
"im +chat-messages-list",
|
||||
"im +chat-search",
|
||||
"im +messages-mget",
|
||||
"im +messages-search",
|
||||
"im +messages-send",
|
||||
"im messages forward",
|
||||
"schema im.messages.forward",
|
||||
"schema im.messages.search"
|
||||
]
|
||||
}
|
||||
20
harness-opt/rounds/round-003/child-cache.json
Normal file
20
harness-opt/rounds/round-003/child-cache.json
Normal file
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"1": {
|
||||
"score": 1.0,
|
||||
"passed": true,
|
||||
"context_window": 33840,
|
||||
"token_usage": 237434,
|
||||
"duration_ms": 44127,
|
||||
"tool_call_count": 25,
|
||||
"feedback": "执行者成功完成了所有期望:首先搜索联系人获取 open_id(首次搜索用单字失败后改为双字搜索成功),然后使用 --as user 创建群组并添加成员,最后发送消息并返回 message_id。整个流程正确,使用了等效的 `--as user` 身份,符合用户「使用我的身份」的要求。验证结果确认所有操作均已生效。"
|
||||
},
|
||||
"3": {
|
||||
"score": 1.0,
|
||||
"passed": true,
|
||||
"context_window": 35942,
|
||||
"token_usage": 234388,
|
||||
"duration_ms": 43185,
|
||||
"tool_call_count": 22,
|
||||
"feedback": "执行者正确理解用户意图,使用用户身份创建群并发送卡片消息。创建群组一次成功,发送卡片经历了4次格式试错(最初使用顶层 elements 和 tag:markdown,后通过查阅官方文档找到正确格式:body.elements + div + lark_md),最终成功发送并返回 message_id。试错后自行纠正符合评判原则,不构成判罚依据。\n- {'reason': '建议在 lark-im-messages-send.md 中增加飞书 interactive card 的标准格式示例,特别是 2.0 schema 下的 body.elements 中使用 div + lark_md 的正确写法,减少 AI 试错成本'}\n- {'reason': '建议 CLI 在遇到 230099 卡片格式错误时,尝试解析并返回更具体的字段级错误提示(如提示 \"elements 应在 body 内\" 或 \"tag:markdown 不被支持\"),帮助 AI 更快定位问题'}"
|
||||
}
|
||||
}
|
||||
119
harness-opt/rounds/round-003/diagnosis.md
Normal file
119
harness-opt/rounds/round-003/diagnosis.md
Normal file
@@ -0,0 +1,119 @@
|
||||
# Round 3 归因(parent=557349b…(round-2 已采纳候选);候选模块见 candidate_modules,由 candidate-writer 据诊断+reach 点名)
|
||||
|
||||
> 目标(objective.json):**在不回退成功率的前提下降低 lark-im skill 文档的 token 成本**。effect 是硬门槛、不可退化;token 与 duration 是并列成本杆。tier=T1,仅可改 `skills/lark-im/**`。target_axis=token。
|
||||
> 判分点只当「什么算挂」的锚,不抄 grader 药方。
|
||||
|
||||
## ⚠️ trace 与当前文件的版本错位(先看,决定本轮抓手是否还在)
|
||||
|
||||
**本轮派工单 trace = round-1 的全 3 题 child-runs**(round-2 只评了 080,故用 round-1 作最近的全覆盖代理)。这些 trace 里的 reference 体积是 **round-1/round-2 改动之前** 的旧版。我用 session-analyze 所用的同一 ai-tokenizer 实测了**当前工作树**文件,确认两者错位如下:
|
||||
|
||||
| 文件 | trace 内体积(旧版,Read 计) | 当前实测(raw / Read 计) | 已被哪轮收割 |
|
||||
|---|---|---|---|
|
||||
| `SKILL.md`(Skill 注入正文) | 3,455–3,456 tok | 3,525 raw | round-1(API Resources/权限表→schema 指针) |
|
||||
| `references/lark-im-messages-send.md` | **5,365 tok** | **2,006 raw / 2,194 Read** | **round-2(5,365→2,006,已收割)** |
|
||||
| `references/lark-im-chat-create.md` | 3,060–3,062 tok | **2,336 raw / 2,645 Read** | **未动过(2023 至今原样),唯一未收割** |
|
||||
|
||||
**含义**:round-2 诊断里的 **RC-2(messages-send.md 内部冗余)已经在 round-2 被采纳并收割**(5,365→2,006),它不再是本轮抓手——不要据 trace 里的 5,365 重复提一遍。本轮 trace 里那块 5,365 是历史值,当前已不存在。**reach>0 集合里唯一还没被压过的干净文件就是 `chat-create.md`**(round-2 的 RC-3)。
|
||||
|
||||
## 跨 case 共同根因(优先看;按对 TOKEN 目标的杠杆排序)
|
||||
|
||||
### RC-1(token,本轮头号且基本是唯一的干净抓手,reach=0.667:014+080)—— `chat-create.md` 内部存在「示例罗列 + 场景重复 + --help 镜像」三类可压缩冗余,且从未被优化过
|
||||
- **现象**:`chat-create.md` 当前 2,336 raw tok(Read 计 ~2,645),是 reach>0 集合里**唯一未被任何轮收割**的 reference。section 级实测分布(raw tok):
|
||||
|
||||
| section | tok | 性质 |
|
||||
|---|---|---|
|
||||
| header(1-11) | 198 | 载重(scope/映射),保留 |
|
||||
| **Commands(12-50) 12 个 bash 示例** | **425** | **过度罗列**:多条仅差一个 flag(`--owner` / `--users` / `--bots` / `--as bot` / `--as user` / `--dry-run` 各一例),信息已在 Parameters 表里 |
|
||||
| Parameters 表(52-69) | 500 | 多数载重;`--chat-mode` 的 L68 长注解与表内 L62 行语义重复 |
|
||||
| AI Usage Guidance(70-108) | 442 | **载重**(232043 两步流是 080/014 路由依据),但表述偏长 |
|
||||
| Output Fields(109-119) | 126 | 载重 |
|
||||
| **Usage Scenarios(120-143) 3 个场景** | **198** | **重复**:Scenario 1/2 重复 Commands 已展示的 `--owner`/`--users`/`--bots` 组合;Scenario 3 重复 messages-send 的串联用法 |
|
||||
| **Common Errors(144-158) 9 行** | **395** | **部分 --help 镜像**:多行直接复述确定性 validation 字符串(`--name exceeds 60`、`--users exceeds 50`、`invalid user id` 等),这些 `--help` / 报错本身就会原样吐出 |
|
||||
| References(159-163) | 44 | 载重 |
|
||||
|
||||
- **这正是 round-2 已经在 messages-send.md 上验证过、且被采纳的同一套压缩模式**:round-2 把 messages-send.md 的「4 处重复选型规则 + 全媒体形态 Commands + --help 镜像」压成「保留载重规则 + 一句 `--help` 指针」(5,365→2,006,被采纳)。chat-create.md 的 Commands(425)↔Usage Scenarios(198) 重叠、Common Errors(395) 的 validation 镜像,是同型冗余。
|
||||
- **可压缩量级(粗估,非药方)**:可压缩质量集中在 Commands+Usage Scenarios 的重叠(合计 ~623 tok,去重后可省一大半)+ Common Errors 的 --help 镜像行。**保守估计可从 2,336 压到 ~1,500–1,700 raw tok(省约 600–800 tok,约 30%)**,与 messages-send.md 的压缩比同量级。具体改法与确切降幅由 candidate-writer 决定、评测裁决。
|
||||
- **载重红线(candidate-writer 取舍时的 effect 风险点,不是 RC-1 不成立)**:AI Usage Guidance 的 **232043 两步流 + `succeed_type=1`**、`--chat-mode topic` vs 普通群+话题消息模式的区分、`--owner` 默认行为,是 014/080 走通 bot 身份建群的语义依据,**不能在压缩中误删**。这条 reference 被 080 实读且 080 据它建群成功(`ok:true`),所以 effect 风险真实存在——压的是示例/场景/报错镜像的体积,不是语义规则。
|
||||
- **axis=token**。可信度=**按需读取**(reach=0.667,子集=014+080,2 题)。压它的降幅只在这 2 题子集里计入,不被 015(没读它)稀释;但子集仅 2 题、且 014 是「读了没用上」(授权阻断没走到建群),实际吃到压缩收益的稳态题只有 080 一题——**证据基数小,降幅需评测在子集上确认**(见数据缺口)。
|
||||
|
||||
### RC-2(token,已收割,本轮不再是抓手)—— messages-send.md 的内部冗余 round-2 已压掉
|
||||
- round-2 RC-2 已被采纳:messages-send.md 5,365→2,006 raw。**本轮不要据 trace 里的 5,365 重复提**。当前 messages-send.md 已是「载重规则 + `--help` 指针」形态,无明显二次压缩空间(剩余多为 content-flag 选型、@mention、media 约束等载重内容)。reach=0.333(仅 080)。
|
||||
|
||||
### RC-3(token,无 T1 干净抓手)—— SKILL.md 常驻正文 round-1 已压过,剩余多为载重 identity/路由
|
||||
- SKILL.md 经 Skill 工具每题必加载(reach=1.0),当前 3,525 raw tok(round-1 已把 API Resources/权限表折叠成 schema 指针)。剩余 `## Important Notes`(L36–85) 各小节(Sender Name Resolution / message enrichment / `--download-resources` / Card / Flag / Feed Shortcut)与 `## Shortcuts` 全表(L87–115) 虽本轮 3 题低命中,但它们是**全域 identity/路由/约束**——这是 round-1 已经动过一刀的同一文件,**再压属同向继续、但删错会碰坏 015/080 已走通的 bot 身份与命令路由判断**(effect 风险高于 RC-1)。**列为更次级、风险更高的抓手**,不作为本轮首选;若要动须只删本轮已确证低命中且非路由的纯枚举行,谨慎程度高于 chat-create。
|
||||
|
||||
## 命令失败热点(跨 case;失败类型由我从 timeline 命令串读出,非判决数字)
|
||||
|
||||
| lark-cli 命令 | 失败次数 | 涉及题数 | 主要失败类型 | 指向的文档问题 |
|
||||
|---|---|---|---|---|
|
||||
| `contact +search-user` | 4 | 1 (014) | bot exit2(invalid_argument) ×2;`--as user` token_missing ×2 | **跨 lark-contact 域**,非 lark-im 内容 |
|
||||
| `auth qrcode --output <绝对/沙箱外路径>` | 1 | 1 (014) | unsafe output path,改相对路径重试成功 | 路径约束在 lark-shared(不可改) |
|
||||
| `im +messages-search` | 2 | 1 (015) | bot exit2 + `--as user` exit2 | 该命令 user-only(SKILL 表已注明);bot 身份必败,agent 没看清就猜 |
|
||||
| `im +chat-messages-list --page-all` | 1 | 1 (015) | exit2(无过滤 page-all) | 见下「015 token 黑洞」 |
|
||||
- **解读**:本轮**没有一条 lark-im 命令因「参数名/类型写错」系统性失败**。080 三条命令 0 失败;014 的失败全在跨域 contact + auth;015 的失败集中在 messages-search(user-only,bot 必败)与无过滤 page-all。**没有 lark-im 侧常规「报错/参数整形」工单**——与 token 减体积方向一致,本轮抓手是减体积不是补内容。
|
||||
|
||||
## 015 的 token 黑洞(与 round-2 一致,复述以免被误当成 token 抓手)
|
||||
- 015 真正的 token 大头**不是任何 lark-im doc**,而是 **block #19:一次 `Read` 工具读入 22,556 tok(占该题 visible 51.5%)**。成因链:#12/#17 `+messages-search`/`--page-all` exit2 → #18 退到 `+chat-messages-list`(无过滤)→ 输出 43.5KB 被持久化 → agent `Read` 整文件 → 22.5k tok 灌进上下文 → 再靠本地 grep(#27–33) 抠出「飞豆」两条。
|
||||
- **从文档角度**:`chat-messages-list.md` 本题 reach=0(状态①:调用前从没读),它本写了 `--start/--end`、`--page-size` 等可避免全量拉取的约束。**但补它=增常驻/触达内容,与本轮降 token 目标方向相反**(见方向冲突);且 22.5k 是**单次工具输出**(强依赖该群消息量,单题不可外推),不是稳定常驻热点。**结论:观察项,交评测裁决,不作为本轮 token 抓手。**
|
||||
|
||||
## 可发现性时序(约束 5 三态;判「前置能不能救」的决定性证据)
|
||||
> 对每条相关 reference / `--help`,按相对首次失败调用的读取时序统计。`--help` 扫 Bash(本轮 3 题均未跑任何 `--help`)。
|
||||
|
||||
| reference / `--help` | 聚合 reach | ①从没读 | ②失败后才读 | ③读了仍错/卡 | 主导态 → 改动方向 |
|
||||
|---|---|---|---|---|---|
|
||||
| `lark-shared/SKILL.md` | 1.0 | 0 | 0 | — | 三题调用前都读了;014 仍卡(环境,非内容);不可改 |
|
||||
| `chat-create.md` | 0.667 | 0 | 0 | — | 080 调用前读→建群成功;014 调用前读→授权阻断(非 reference 错)。**非触达问题,纯减体积** |
|
||||
| `messages-send.md` | 0.333 | 0 | 0 | — | 080 调用前读→发卡片成功。**非触达问题**(已收割) |
|
||||
| `chat-messages-list.md` | 0.0 | 1 (015) | 0 | — | ① 015 调用前从没读→`--page-all` 全量拉取→token 黑洞。触达缺口,但补它=增 token,与目标冲突 |
|
||||
| `messages-search.md` | 0.0 | 1 (015) | 0 | — | ① 015 从没读,直接猜 `+messages-search` ×2 → exit2(user-only,bot 必败) |
|
||||
- **结论**:**本轮没有「该前置」的干净 case**。RC-1(chat-create.md 减体积)是「调用前已读、内容够用 → 去冗余」的纯 token 减法,不涉及触达。015 的两处 ① 触达缺口确实存在,但修它们=增内容、与降 token 目标相反,且 015 已 PASS(bot + 本地 grep 兜底)——属观察项,**不要被诱导去推前置**。
|
||||
|
||||
## 方向冲突记录(硬性约束 7)
|
||||
- **减体积(RC-1 chat-create.md,与 objective.direction 同向)** vs **补收窄/前置指引(修 015 chat-messages-list 全量灌入,与 objective 反向)**:前者降按需 token,后者为省「单次工具输出」反而要**增**文档常驻 token。两者方向相反,**不可合并**。本轮目标是降 token,取减体积一侧;015 全量灌入作为观察项记录、不作为要补的内容根因。
|
||||
|
||||
## 差距台账复盘
|
||||
- 无(`discard-ledger.json` 为空,无已跑未采纳候选)。
|
||||
|
||||
## 逐 case
|
||||
|
||||
### 1 (014) [workorder=PASS / 实质 FAIL] token=34,555(reported)/visible 17,364 耗时=37s 命令失败率=5/7 维度=效果(不可修)
|
||||
- 判分点结果:3 条全 ✗——建群/拉人/发消息全未发生,卡在 `contact +search-user` 解析 open_id(user 授权阻断 + 跨域 contact)。verdict=PASS 系聚合口径,按判分点证据当 FAIL 处理。
|
||||
- 命令失败:5/7。`contact +search-user` bot exit2 ×2、`--as user` token_missing ×2;`auth qrcode` 绝对路径 unsafe ×1(改相对路径成功)。**全部非 lark-im 命令**。
|
||||
- 可发现性时序:#4 读 SKILL.md 正文(3,456) + #6 读 lark-shared(3,751,跨 skill) + #7 读 chat-create.md(3,062,调用前已读);失败在更上游的跨域 contact + 授权。**非 lark-im 触达问题**。
|
||||
- token 归因:SKILL.md 正文 3,456(常驻静态,19.9%)+ lark-shared 3,751(**跨 skill,不归因 lark-im**)+ chat-create.md 3,062(按需,17.6%,**本题读了没用上**——授权阻断没走到建群)+ 系统 Skill 列表注入 4,612(固定开销,不归因)。lark-cli 命令累计含多次短失败回显,单条都短、非热点。
|
||||
- 耗时归因:本题往返多(查联系人→切 contact→失败→auth status→授权→qrcode 重试);多为授权链路 + 跨域固有串行 + 反应式重试(duration 弱信号,需多轮复现)。
|
||||
- 文档根因:效果=沙箱 user 授权 + 跨域 contact(环境,**无 lark-im 文档根因,本轮不改**);token=chat-create.md 按需冗余(RC-1,但本题读了没用上,收益只在 080 这种走通题里兑现)+ SKILL.md 常驻(RC-3,风险高、次级)。
|
||||
|
||||
### 2 (015) [PASS·真] token=54,568(reported)/visible 43,760 耗时=2m5s 命令失败率=3/11 维度=token(但大头非 lark-im doc)
|
||||
- 判分点结果:3/3 ✓——定位群、转发「飞豆」消息、@傅六铭知会全部成功(两次 `messages-send` 均 `ok:true`)。**全程 bot 身份,无授权阻断**。
|
||||
- 命令失败:3/11。`+messages-search` bot exit2、`+messages-search --as user` exit2、`+chat-messages-list --page-all` exit2(无过滤);agent 退到无 page-all + 本地 grep 兜底成功。(#14 `--page-all | grep` 返回空属「成功但无命中」,非硬失败,未计入。)
|
||||
- 可发现性时序:① `messages-search.md` / `chat-messages-list.md` 调用前从没读(reach=0),直接猜命令。**本题未读任何 lark-im reference**,故 lark-im reference 的体积与本题 token 无关。
|
||||
- token 归因:**本题 token 大头不是 lark-im doc**,是 block #19 一次 `Read` 持久化文件 = **22,556 tok(51.5%,归「其他工具调用/返回」)**,成因=`--page-all` 无过滤全量拉取→43.5KB→Read 灌入(**单次输出**可信度,强依赖该群消息量)。SKILL.md 正文 3,448(常驻)。lark-shared 3,749(跨 skill,不归因)。**RC-1 改 chat-create.md 对本题 token 无影响**(本题没读它)。
|
||||
- 耗时归因:本题最长(2m5s),主因 messages-search 连环失败→改 page-all→大输出→多次本地 grep 抠数据的多轮往返(duration 弱信号;model calls 16/raw 32,明显高于 080,作旁证)。
|
||||
- 文档根因:token 黑洞的放大器=`chat-messages-list.md` 没被读到(状态①)+ SKILL.md 表未提示大群应 server-side 收窄——但**补这条与降 token 目标相反**(方向张力),列为观察项;本题已 PASS。本轮 token 抓手(RC-1)不落在本题。
|
||||
|
||||
### 3 (080) [PASS·真] token=38,009(reported)/visible 21,599 耗时=47s 命令失败率=0/3 维度=token
|
||||
- 判分点结果:3/3 ✓——`auth status` 见 bot ready→主动选 bot→建群 `ok:true`→发 interactive 卡片 `ok:true`。**任务完整完成,零命令失败**。
|
||||
- 命令失败:0/3。三条 lark-cli(auth status / chat-create / messages-send)全成功。
|
||||
- 可发现性时序:#4 读 SKILL.md 正文(3,455) + #6 读 lark-shared(3,751,跨 skill) + #9 读 chat-create.md(3,060) + #10 读 messages-send.md(5,365,旧版) ,全部状态③(调用前已读且用上)。**无触达问题。** 实际只用了 `+chat-create --name … --format json` 的最简形态——没用两步流/owner/members/topic/error-recovery。
|
||||
- token 归因:**本题是纯 token 抓手题**——读取 Skill 占 56.4%:messages-send.md 5,365(trace 旧版,**当前已被 round-2 压到 2,006,本轮不再可压**)+ SKILL.md 正文 3,455(常驻,RC-3)+ chat-create.md 3,060(按需,**RC-1,当前 2,336,本轮唯一干净抓手**)。系统 Skill 列表注入 4,612(固定开销,不归因)。lark-shared 3,751(跨 skill,不归因)。
|
||||
- 耗时归因:47s,全部为正常建群+发卡片串行,无重试、无写后回查(无离群)。
|
||||
- 文档根因:无效果根因(已绿);token=RC-1(chat-create.md 内部冗余,本题是其收益唯一稳态兑现题)+ RC-3(SKILL.md 常驻,风险高、次级)。**本题 token 杠杆最清晰且 effect 风险可控**(命令全成功,压 chat-create.md 的示例/场景/报错镜像不碰 080 实际用到的最简建群链路)。
|
||||
|
||||
## 给 candidate-writer 的收口(不含具体改法)
|
||||
- **唯一在 T1 内还没被收割的干净 token 抓手是 RC-1(`chat-create.md` 内部冗余)**:Commands 12 例过度罗列 + Usage Scenarios 3 场景重复 Commands + Common Errors 9 行部分镜像 validation 字符串——**与 round-2 已采纳的 messages-send.md 压缩同型**,粗估可省 ~600–800 raw tok(约 30%)。reach=0.667(014+080),降幅在子集计入。
|
||||
- **载重红线**:AI Usage Guidance 的 232043 两步流 + `succeed_type=1` + `--chat-mode topic` 区分 + `--owner` 默认,是 080/014 走通 bot 建群的语义依据,**压缩中不可误删**——压的是示例/场景/报错镜像体积,不是规则。
|
||||
- **RC-2 已收割**:messages-send.md round-2 已 5,365→2,006,trace 里的 5,365 是历史值,**不要重复提**。
|
||||
- **RC-3(SKILL.md 常驻)是次级且风险更高**:round-1 已压过一刀,剩余多为全域 identity/路由/约束,删错碰坏 015/080 已走通的 bot 身份与命令路由——不作首选。
|
||||
- **不要推前置**:本轮没有「该前置」的干净 case。015 的两处 ① 触达缺口(chat-messages-list/messages-search 没读)虽真实,但修=增内容、与降 token 反向,且 015 已 PASS——属观察项。
|
||||
- **effect 不可在本轮 T1 内合法抬升**:014 是环境(沙箱不能扫码)+ 跨域 contact,无 lark-im 文档根因;015/080 已真 PASS。effect deltas 视作 auth-noise,不追。
|
||||
- **干净 token 抓手接近见底(诚实判断)**:reach>0 集合三个文件中,messages-send.md(round-2)与 SKILL.md(round-1)已各压一刀,**chat-create.md 是最后一个未动过的干净文件**。压完它之后,T1 内 reach>0 的纯冗余(罗列/重复/--help 镜像)基本耗尽;再往下只剩 (a) 高 effect 风险的 SKILL.md 载重内容,或 (b) reach=0 的 22 个盲区 reference(压了也不在判决集、无法被采纳)。**本轮 RC-1 很可能是这条优化路径上最后一个低风险、可被采纳的 token 抓手。**
|
||||
- **缺失信息(doc_fix_hint 语气,非药方)**:chat-create.md 把同一组 flag 在 Commands(12 例) 与 Usage Scenarios(3 场景) 重复演示、Common Errors 多行复述 `--help`/报错本身就会吐的 validation 字符串——这类「枚举/重复/镜像、低增量」内容是其 token 的主要去处,且是减法(删冗余)而非加法。
|
||||
|
||||
## 数据缺口
|
||||
1. **trace 版本错位(最关键)**:本轮 trace=round-1 旧版 child-runs,messages-send.md 在 trace 里仍是 5,365(round-2 已压到 2,006)。所有「当前文件体积」结论我已用 ai-tokenizer 实测当前工作树校正(SKILL.md 3,525 / chat-create.md 2,336 / messages-send.md 2,006),但**单题行为与 reach 仍来自旧 trace**——若 round-2 改动改变了 080/014 的读取行为,需以实际 round-3 eval-run 复核。
|
||||
2. **RC-1 子集小**:chat-create.md reach=0.667 但实际吃到压缩收益的稳态题只有 080(014 读了没用上、授权阻断),证据基数=1,降幅需评测在子集确认。
|
||||
3. **015 的 22.5k 黑洞是单次工具输出**,强依赖该群消息量,非稳定常驻热点,单题不可外推;且与降 token 目标方向冲突,不作抓手。
|
||||
4. **duration 三题波动大**(37s/2m5s/47s),015 长尾主因 messages-search 连环失败+大输出多轮抠数据;单轮不足定论,需多轮复现。model calls(8/16/6) 比 wall-clock 稳,可作旁证。
|
||||
5. **工具调用口径不一致**:trend.json 的 R1 tool_calls=26.3、R2=10,与 session-analyze 的 model calls(8/16/6) 口径不同(趋势表疑似含 raw 计数);旁证以 timeline 实际往返为准。趋势看:R1→R2 命令失败率 0.60→0.35、tool_calls 26→10 明显下降,但那主要是 effect 从「三题全卡授权」变成「2 真 PASS + 1 卡」带来的,**不是 token 改动的功劳**;token 均值 R1 31,997→R2 42,377 上升,主因是 R2 只评 080(单题大)口径差异 + 015 黑洞,非文档常驻变重——趋势对 token 轴判读价值有限,以单题 session-analyze 为准。
|
||||
6. **effect 维度全部归因为「无文档根因/不可修」**:014 跨域+环境,015/080 已绿。本轮 effect 无 T1 可发力点,deltas 视作 auth-noise。
|
||||
1
harness-opt/rounds/round-003/discard-ledger.json
Normal file
1
harness-opt/rounds/round-003/discard-ledger.json
Normal file
@@ -0,0 +1 @@
|
||||
[]
|
||||
1
harness-opt/rounds/round-003/failure-memory.json
Normal file
1
harness-opt/rounds/round-003/failure-memory.json
Normal file
@@ -0,0 +1 @@
|
||||
[]
|
||||
220
harness-opt/rounds/round-003/module-reach.json
Normal file
220
harness-opt/rounds/round-003/module-reach.json
Normal file
@@ -0,0 +1,220 @@
|
||||
{
|
||||
"skills/lark-im/SKILL.md": {
|
||||
"reach": 1.0,
|
||||
"read_cases": [
|
||||
"1",
|
||||
"2",
|
||||
"3"
|
||||
],
|
||||
"actual_cases": [
|
||||
"1",
|
||||
"2",
|
||||
"3"
|
||||
],
|
||||
"expected_cases": [],
|
||||
"discoverability_miss": [],
|
||||
"is_domain_skill": true
|
||||
},
|
||||
"skills/lark-im/references/lark-im-chat-create.md": {
|
||||
"reach": 0.667,
|
||||
"read_cases": [
|
||||
"1",
|
||||
"3"
|
||||
],
|
||||
"actual_cases": [
|
||||
"1",
|
||||
"3"
|
||||
],
|
||||
"expected_cases": [],
|
||||
"discoverability_miss": [],
|
||||
"is_domain_skill": false
|
||||
},
|
||||
"skills/lark-im/references/lark-im-chat-identity.md": {
|
||||
"reach": 0.0,
|
||||
"read_cases": [],
|
||||
"actual_cases": [],
|
||||
"expected_cases": [],
|
||||
"discoverability_miss": [],
|
||||
"is_domain_skill": false
|
||||
},
|
||||
"skills/lark-im/references/lark-im-chat-list.md": {
|
||||
"reach": 0.0,
|
||||
"read_cases": [],
|
||||
"actual_cases": [],
|
||||
"expected_cases": [],
|
||||
"discoverability_miss": [],
|
||||
"is_domain_skill": false
|
||||
},
|
||||
"skills/lark-im/references/lark-im-chat-messages-list.md": {
|
||||
"reach": 0.0,
|
||||
"read_cases": [],
|
||||
"actual_cases": [],
|
||||
"expected_cases": [],
|
||||
"discoverability_miss": [],
|
||||
"is_domain_skill": false
|
||||
},
|
||||
"skills/lark-im/references/lark-im-chat-search.md": {
|
||||
"reach": 0.0,
|
||||
"read_cases": [],
|
||||
"actual_cases": [],
|
||||
"expected_cases": [],
|
||||
"discoverability_miss": [],
|
||||
"is_domain_skill": false
|
||||
},
|
||||
"skills/lark-im/references/lark-im-chat-update.md": {
|
||||
"reach": 0.0,
|
||||
"read_cases": [],
|
||||
"actual_cases": [],
|
||||
"expected_cases": [],
|
||||
"discoverability_miss": [],
|
||||
"is_domain_skill": false
|
||||
},
|
||||
"skills/lark-im/references/lark-im-feed-group-list-item.md": {
|
||||
"reach": 0.0,
|
||||
"read_cases": [],
|
||||
"actual_cases": [],
|
||||
"expected_cases": [],
|
||||
"discoverability_miss": [],
|
||||
"is_domain_skill": false
|
||||
},
|
||||
"skills/lark-im/references/lark-im-feed-group-list.md": {
|
||||
"reach": 0.0,
|
||||
"read_cases": [],
|
||||
"actual_cases": [],
|
||||
"expected_cases": [],
|
||||
"discoverability_miss": [],
|
||||
"is_domain_skill": false
|
||||
},
|
||||
"skills/lark-im/references/lark-im-feed-group-query-item.md": {
|
||||
"reach": 0.0,
|
||||
"read_cases": [],
|
||||
"actual_cases": [],
|
||||
"expected_cases": [],
|
||||
"discoverability_miss": [],
|
||||
"is_domain_skill": false
|
||||
},
|
||||
"skills/lark-im/references/lark-im-feed-groups.md": {
|
||||
"reach": 0.0,
|
||||
"read_cases": [],
|
||||
"actual_cases": [],
|
||||
"expected_cases": [],
|
||||
"discoverability_miss": [],
|
||||
"is_domain_skill": false
|
||||
},
|
||||
"skills/lark-im/references/lark-im-feed-shortcut-create.md": {
|
||||
"reach": 0.0,
|
||||
"read_cases": [],
|
||||
"actual_cases": [],
|
||||
"expected_cases": [],
|
||||
"discoverability_miss": [],
|
||||
"is_domain_skill": false
|
||||
},
|
||||
"skills/lark-im/references/lark-im-feed-shortcut-list.md": {
|
||||
"reach": 0.0,
|
||||
"read_cases": [],
|
||||
"actual_cases": [],
|
||||
"expected_cases": [],
|
||||
"discoverability_miss": [],
|
||||
"is_domain_skill": false
|
||||
},
|
||||
"skills/lark-im/references/lark-im-feed-shortcut-remove.md": {
|
||||
"reach": 0.0,
|
||||
"read_cases": [],
|
||||
"actual_cases": [],
|
||||
"expected_cases": [],
|
||||
"discoverability_miss": [],
|
||||
"is_domain_skill": false
|
||||
},
|
||||
"skills/lark-im/references/lark-im-flag-cancel.md": {
|
||||
"reach": 0.0,
|
||||
"read_cases": [],
|
||||
"actual_cases": [],
|
||||
"expected_cases": [],
|
||||
"discoverability_miss": [],
|
||||
"is_domain_skill": false
|
||||
},
|
||||
"skills/lark-im/references/lark-im-flag-create.md": {
|
||||
"reach": 0.0,
|
||||
"read_cases": [],
|
||||
"actual_cases": [],
|
||||
"expected_cases": [],
|
||||
"discoverability_miss": [],
|
||||
"is_domain_skill": false
|
||||
},
|
||||
"skills/lark-im/references/lark-im-flag-list.md": {
|
||||
"reach": 0.0,
|
||||
"read_cases": [],
|
||||
"actual_cases": [],
|
||||
"expected_cases": [],
|
||||
"discoverability_miss": [],
|
||||
"is_domain_skill": false
|
||||
},
|
||||
"skills/lark-im/references/lark-im-message-enrichment.md": {
|
||||
"reach": 0.0,
|
||||
"read_cases": [],
|
||||
"actual_cases": [],
|
||||
"expected_cases": [],
|
||||
"discoverability_miss": [],
|
||||
"is_domain_skill": false
|
||||
},
|
||||
"skills/lark-im/references/lark-im-messages-mget.md": {
|
||||
"reach": 0.0,
|
||||
"read_cases": [],
|
||||
"actual_cases": [],
|
||||
"expected_cases": [],
|
||||
"discoverability_miss": [],
|
||||
"is_domain_skill": false
|
||||
},
|
||||
"skills/lark-im/references/lark-im-messages-reply.md": {
|
||||
"reach": 0.0,
|
||||
"read_cases": [],
|
||||
"actual_cases": [],
|
||||
"expected_cases": [],
|
||||
"discoverability_miss": [],
|
||||
"is_domain_skill": false
|
||||
},
|
||||
"skills/lark-im/references/lark-im-messages-resources-download.md": {
|
||||
"reach": 0.0,
|
||||
"read_cases": [],
|
||||
"actual_cases": [],
|
||||
"expected_cases": [],
|
||||
"discoverability_miss": [],
|
||||
"is_domain_skill": false
|
||||
},
|
||||
"skills/lark-im/references/lark-im-messages-search.md": {
|
||||
"reach": 0.0,
|
||||
"read_cases": [],
|
||||
"actual_cases": [],
|
||||
"expected_cases": [],
|
||||
"discoverability_miss": [],
|
||||
"is_domain_skill": false
|
||||
},
|
||||
"skills/lark-im/references/lark-im-messages-send.md": {
|
||||
"reach": 0.333,
|
||||
"read_cases": [
|
||||
"3"
|
||||
],
|
||||
"actual_cases": [
|
||||
"3"
|
||||
],
|
||||
"expected_cases": [],
|
||||
"discoverability_miss": [],
|
||||
"is_domain_skill": false
|
||||
},
|
||||
"skills/lark-im/references/lark-im-reactions.md": {
|
||||
"reach": 0.0,
|
||||
"read_cases": [],
|
||||
"actual_cases": [],
|
||||
"expected_cases": [],
|
||||
"discoverability_miss": [],
|
||||
"is_domain_skill": false
|
||||
},
|
||||
"skills/lark-im/references/lark-im-threads-messages-list.md": {
|
||||
"reach": 0.0,
|
||||
"read_cases": [],
|
||||
"actual_cases": [],
|
||||
"expected_cases": [],
|
||||
"discoverability_miss": [],
|
||||
"is_domain_skill": false
|
||||
}
|
||||
}
|
||||
18
harness-opt/rounds/round-003/review.json
Normal file
18
harness-opt/rounds/round-003/review.json
Normal file
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"generated_by": "lark-cli-harness:opt-reviewer",
|
||||
"verdict": "PASS",
|
||||
"module": "skills/lark-im/references/lark-im-chat-create.md",
|
||||
"tier": "T1",
|
||||
"round_index": 3,
|
||||
"base_commit": "572eb8da41f608bd93b25916cac02cb772825b97",
|
||||
"code_tip": "cbd6e56ac07285fd973c53ff7382da0112b6cf5d",
|
||||
"reason": "纯瘦身改动,对抗式逐项核验未发现可证伪点:14 条承重红线(232043 两步流/succeed_type=1/chat-mode topic 与 thread 区分/--owner 默认/set-bot-manager/chat.members create/Output Fields/scopes)在改后文件全部 grep 命中;Scenario 3 建群→欢迎语 recipe 逐字保留仅换标题、搬迁到同文件 AI Usage Guidance 末尾未删;删掉的 6 行 Common Errors 已在 shortcuts/im/im_chat_create.go 源码核实是 CLI 原样回显的确定性 validation 字符串(运行时报错可复得,非仅靠 --help),删掉的命令例均为单 flag 变体且 flag 仍全列于 Parameters 表;字节 7996→6450(-19.3%)/词 1258→969(-23%) 为真实删减、无增读拉力、recipe 在同文件内搬迁不引发额外读;单根因 RC-1(本文件内部冗余),strategy 明确不捆 RC-2/RC-3。",
|
||||
"dimensions": {
|
||||
"reward_hack": {"pass": true, "evidence": "无硬编码评测答案/资源名/ID;未对 080 的 --name --format json 最简建群链做特判,080 链路一环未碰;属通用『删运行时另有出处的重复』瘦身,与 round-2 messages-send 同型同纪律,非针对某几题"},
|
||||
"semantic_regress": {"pass": true, "evidence": "14 条承重红线改后文件全部命中;Scenario 3 recipe 逐字保留(仅换标题、搬入 AI Usage Guidance);删的 6 行报错经 im_chat_create.go 核实为 CLI verbatim validation(运行时可复得);删的命令例均单 flag 变体、flag 仍全列于 Parameters 表,无承重内容丢失"},
|
||||
"token_shift": {"pass": true, "evidence": "真实删减 bytes 7996→6450(-19.3%)、words -23%;纯删除无新增前置/『先读 X』拉力;welcome recipe 在同一文件内搬迁不触发额外读;唯一 --help 指针仅覆盖 Parameters 表已列的单 flag 变体,非强制增读。运行时每题 context 只降不升"},
|
||||
"contract_break": {"pass": true, "evidence": "T1 文档无对外契约;结构完整(仅 Usage Scenarios 段:2 重复删、recipe 搬迁),所有 ## 章节与 References 链接保留,无断链/缺章"},
|
||||
"devguide": {"pass": true, "evidence": "对照 review-rubric 优化红线(semantic_regress / contract_break 两维):未删承重、未破坏结构;reference 收敛到 gotcha-only、与 --help/Parameters 重复内容下沉为指针,符合 optimization-playbook 的『单命令示例下沉、与 --help 重复留一处其余指针』;annotation 三段均标 R1 落在可重构范围、未触 R3 的 AI Usage Guidance prose"},
|
||||
"single_root_cause":{"pass": true, "evidence": "diff 只服务 RC-1(本文件内部『示例罗列+场景重复+报错镜像』三类冗余),全为同一根因下的去重;未捆 RC-2(messages-send)/RC-3(SKILL.md),未夹带语义独立的承重删除,无多根因对冲叙事"}
|
||||
}
|
||||
}
|
||||
394
harness-opt/rounds/round-003/round.json
Normal file
394
harness-opt/rounds/round-003/round.json
Normal file
@@ -0,0 +1,394 @@
|
||||
{
|
||||
"round": 3,
|
||||
"status": "admitted",
|
||||
"parent_id": "557349b40feb359bb791749a37571d59edb7e72e",
|
||||
"parent_worktree": "/Users/bytedance/Projects/cli",
|
||||
"child_worktree": "/Users/bytedance/Projects/cli",
|
||||
"base_commit": "572eb8da41f608bd93b25916cac02cb772825b97",
|
||||
"module": "skills/lark-im/references/lark-im-chat-create.md",
|
||||
"candidate_modules": [
|
||||
"skills/lark-im/SKILL.md",
|
||||
"skills/lark-im/references/lark-im-chat-create.md",
|
||||
"skills/lark-im/references/lark-im-chat-identity.md",
|
||||
"skills/lark-im/references/lark-im-chat-list.md",
|
||||
"skills/lark-im/references/lark-im-chat-messages-list.md",
|
||||
"skills/lark-im/references/lark-im-chat-search.md",
|
||||
"skills/lark-im/references/lark-im-chat-update.md",
|
||||
"skills/lark-im/references/lark-im-feed-group-list-item.md",
|
||||
"skills/lark-im/references/lark-im-feed-group-list.md",
|
||||
"skills/lark-im/references/lark-im-feed-group-query-item.md",
|
||||
"skills/lark-im/references/lark-im-feed-groups.md",
|
||||
"skills/lark-im/references/lark-im-feed-shortcut-create.md",
|
||||
"skills/lark-im/references/lark-im-feed-shortcut-list.md",
|
||||
"skills/lark-im/references/lark-im-feed-shortcut-remove.md",
|
||||
"skills/lark-im/references/lark-im-flag-cancel.md",
|
||||
"skills/lark-im/references/lark-im-flag-create.md",
|
||||
"skills/lark-im/references/lark-im-flag-list.md",
|
||||
"skills/lark-im/references/lark-im-message-enrichment.md",
|
||||
"skills/lark-im/references/lark-im-messages-mget.md",
|
||||
"skills/lark-im/references/lark-im-messages-reply.md",
|
||||
"skills/lark-im/references/lark-im-messages-resources-download.md",
|
||||
"skills/lark-im/references/lark-im-messages-search.md",
|
||||
"skills/lark-im/references/lark-im-messages-send.md",
|
||||
"skills/lark-im/references/lark-im-reactions.md",
|
||||
"skills/lark-im/references/lark-im-threads-messages-list.md"
|
||||
],
|
||||
"module_reach": {
|
||||
"skills/lark-im/SKILL.md": {
|
||||
"reach": 1.0,
|
||||
"read_cases": [
|
||||
"1",
|
||||
"2",
|
||||
"3"
|
||||
],
|
||||
"actual_cases": [
|
||||
"1",
|
||||
"2",
|
||||
"3"
|
||||
],
|
||||
"expected_cases": [],
|
||||
"discoverability_miss": [],
|
||||
"is_domain_skill": true
|
||||
},
|
||||
"skills/lark-im/references/lark-im-chat-create.md": {
|
||||
"reach": 0.667,
|
||||
"read_cases": [
|
||||
"1",
|
||||
"3"
|
||||
],
|
||||
"actual_cases": [
|
||||
"1",
|
||||
"3"
|
||||
],
|
||||
"expected_cases": [],
|
||||
"discoverability_miss": [],
|
||||
"is_domain_skill": false
|
||||
},
|
||||
"skills/lark-im/references/lark-im-chat-identity.md": {
|
||||
"reach": 0.0,
|
||||
"read_cases": [],
|
||||
"actual_cases": [],
|
||||
"expected_cases": [],
|
||||
"discoverability_miss": [],
|
||||
"is_domain_skill": false
|
||||
},
|
||||
"skills/lark-im/references/lark-im-chat-list.md": {
|
||||
"reach": 0.0,
|
||||
"read_cases": [],
|
||||
"actual_cases": [],
|
||||
"expected_cases": [],
|
||||
"discoverability_miss": [],
|
||||
"is_domain_skill": false
|
||||
},
|
||||
"skills/lark-im/references/lark-im-chat-messages-list.md": {
|
||||
"reach": 0.0,
|
||||
"read_cases": [],
|
||||
"actual_cases": [],
|
||||
"expected_cases": [],
|
||||
"discoverability_miss": [],
|
||||
"is_domain_skill": false
|
||||
},
|
||||
"skills/lark-im/references/lark-im-chat-search.md": {
|
||||
"reach": 0.0,
|
||||
"read_cases": [],
|
||||
"actual_cases": [],
|
||||
"expected_cases": [],
|
||||
"discoverability_miss": [],
|
||||
"is_domain_skill": false
|
||||
},
|
||||
"skills/lark-im/references/lark-im-chat-update.md": {
|
||||
"reach": 0.0,
|
||||
"read_cases": [],
|
||||
"actual_cases": [],
|
||||
"expected_cases": [],
|
||||
"discoverability_miss": [],
|
||||
"is_domain_skill": false
|
||||
},
|
||||
"skills/lark-im/references/lark-im-feed-group-list-item.md": {
|
||||
"reach": 0.0,
|
||||
"read_cases": [],
|
||||
"actual_cases": [],
|
||||
"expected_cases": [],
|
||||
"discoverability_miss": [],
|
||||
"is_domain_skill": false
|
||||
},
|
||||
"skills/lark-im/references/lark-im-feed-group-list.md": {
|
||||
"reach": 0.0,
|
||||
"read_cases": [],
|
||||
"actual_cases": [],
|
||||
"expected_cases": [],
|
||||
"discoverability_miss": [],
|
||||
"is_domain_skill": false
|
||||
},
|
||||
"skills/lark-im/references/lark-im-feed-group-query-item.md": {
|
||||
"reach": 0.0,
|
||||
"read_cases": [],
|
||||
"actual_cases": [],
|
||||
"expected_cases": [],
|
||||
"discoverability_miss": [],
|
||||
"is_domain_skill": false
|
||||
},
|
||||
"skills/lark-im/references/lark-im-feed-groups.md": {
|
||||
"reach": 0.0,
|
||||
"read_cases": [],
|
||||
"actual_cases": [],
|
||||
"expected_cases": [],
|
||||
"discoverability_miss": [],
|
||||
"is_domain_skill": false
|
||||
},
|
||||
"skills/lark-im/references/lark-im-feed-shortcut-create.md": {
|
||||
"reach": 0.0,
|
||||
"read_cases": [],
|
||||
"actual_cases": [],
|
||||
"expected_cases": [],
|
||||
"discoverability_miss": [],
|
||||
"is_domain_skill": false
|
||||
},
|
||||
"skills/lark-im/references/lark-im-feed-shortcut-list.md": {
|
||||
"reach": 0.0,
|
||||
"read_cases": [],
|
||||
"actual_cases": [],
|
||||
"expected_cases": [],
|
||||
"discoverability_miss": [],
|
||||
"is_domain_skill": false
|
||||
},
|
||||
"skills/lark-im/references/lark-im-feed-shortcut-remove.md": {
|
||||
"reach": 0.0,
|
||||
"read_cases": [],
|
||||
"actual_cases": [],
|
||||
"expected_cases": [],
|
||||
"discoverability_miss": [],
|
||||
"is_domain_skill": false
|
||||
},
|
||||
"skills/lark-im/references/lark-im-flag-cancel.md": {
|
||||
"reach": 0.0,
|
||||
"read_cases": [],
|
||||
"actual_cases": [],
|
||||
"expected_cases": [],
|
||||
"discoverability_miss": [],
|
||||
"is_domain_skill": false
|
||||
},
|
||||
"skills/lark-im/references/lark-im-flag-create.md": {
|
||||
"reach": 0.0,
|
||||
"read_cases": [],
|
||||
"actual_cases": [],
|
||||
"expected_cases": [],
|
||||
"discoverability_miss": [],
|
||||
"is_domain_skill": false
|
||||
},
|
||||
"skills/lark-im/references/lark-im-flag-list.md": {
|
||||
"reach": 0.0,
|
||||
"read_cases": [],
|
||||
"actual_cases": [],
|
||||
"expected_cases": [],
|
||||
"discoverability_miss": [],
|
||||
"is_domain_skill": false
|
||||
},
|
||||
"skills/lark-im/references/lark-im-message-enrichment.md": {
|
||||
"reach": 0.0,
|
||||
"read_cases": [],
|
||||
"actual_cases": [],
|
||||
"expected_cases": [],
|
||||
"discoverability_miss": [],
|
||||
"is_domain_skill": false
|
||||
},
|
||||
"skills/lark-im/references/lark-im-messages-mget.md": {
|
||||
"reach": 0.0,
|
||||
"read_cases": [],
|
||||
"actual_cases": [],
|
||||
"expected_cases": [],
|
||||
"discoverability_miss": [],
|
||||
"is_domain_skill": false
|
||||
},
|
||||
"skills/lark-im/references/lark-im-messages-reply.md": {
|
||||
"reach": 0.0,
|
||||
"read_cases": [],
|
||||
"actual_cases": [],
|
||||
"expected_cases": [],
|
||||
"discoverability_miss": [],
|
||||
"is_domain_skill": false
|
||||
},
|
||||
"skills/lark-im/references/lark-im-messages-resources-download.md": {
|
||||
"reach": 0.0,
|
||||
"read_cases": [],
|
||||
"actual_cases": [],
|
||||
"expected_cases": [],
|
||||
"discoverability_miss": [],
|
||||
"is_domain_skill": false
|
||||
},
|
||||
"skills/lark-im/references/lark-im-messages-search.md": {
|
||||
"reach": 0.0,
|
||||
"read_cases": [],
|
||||
"actual_cases": [],
|
||||
"expected_cases": [],
|
||||
"discoverability_miss": [],
|
||||
"is_domain_skill": false
|
||||
},
|
||||
"skills/lark-im/references/lark-im-messages-send.md": {
|
||||
"reach": 0.333,
|
||||
"read_cases": [
|
||||
"3"
|
||||
],
|
||||
"actual_cases": [
|
||||
"3"
|
||||
],
|
||||
"expected_cases": [],
|
||||
"discoverability_miss": [],
|
||||
"is_domain_skill": false
|
||||
},
|
||||
"skills/lark-im/references/lark-im-reactions.md": {
|
||||
"reach": 0.0,
|
||||
"read_cases": [],
|
||||
"actual_cases": [],
|
||||
"expected_cases": [],
|
||||
"discoverability_miss": [],
|
||||
"is_domain_skill": false
|
||||
},
|
||||
"skills/lark-im/references/lark-im-threads-messages-list.md": {
|
||||
"reach": 0.0,
|
||||
"read_cases": [],
|
||||
"actual_cases": [],
|
||||
"expected_cases": [],
|
||||
"discoverability_miss": [],
|
||||
"is_domain_skill": false
|
||||
}
|
||||
},
|
||||
"expected_reach": {},
|
||||
"minibatch": [
|
||||
"1",
|
||||
"2",
|
||||
"3"
|
||||
],
|
||||
"pareto_cases": [
|
||||
"1",
|
||||
"2",
|
||||
"3"
|
||||
],
|
||||
"artifacts": {
|
||||
"workorder": "workorder.md",
|
||||
"diagnosis": "diagnosis.md",
|
||||
"attribution": "attribution.json",
|
||||
"strategy": "strategy.md",
|
||||
"review": "review.json",
|
||||
"trend": "trend.json"
|
||||
},
|
||||
"code_tip": "cbd6e56ac07285fd973c53ff7382da0112b6cf5d",
|
||||
"signature": "53194d7a111df326cc078b633f43587225bd0132",
|
||||
"tier": "T1",
|
||||
"intent": "dedup Commands<->Usage Scenarios overlap + compress --help-mirroring Common Errors in chat-create.md; keep all red lines (232043 two-step,succeed_type=1,chat-mode topic,--owner)",
|
||||
"target_axis": "token",
|
||||
"changed_files": [
|
||||
"skills/lark-im/references/lark-im-chat-create.md"
|
||||
],
|
||||
"decision_basis": {
|
||||
"type": "module",
|
||||
"module": "skills/lark-im/references/lark-im-chat-create.md"
|
||||
},
|
||||
"decision_cases": [
|
||||
"1",
|
||||
"3"
|
||||
],
|
||||
"review": {
|
||||
"generated_by": "lark-cli-harness:opt-reviewer",
|
||||
"verdict": "PASS",
|
||||
"module": "skills/lark-im/references/lark-im-chat-create.md",
|
||||
"tier": "T1",
|
||||
"round_index": 3,
|
||||
"base_commit": "572eb8da41f608bd93b25916cac02cb772825b97",
|
||||
"code_tip": "cbd6e56ac07285fd973c53ff7382da0112b6cf5d",
|
||||
"reason": "纯瘦身改动,对抗式逐项核验未发现可证伪点:14 条承重红线(232043 两步流/succeed_type=1/chat-mode topic 与 thread 区分/--owner 默认/set-bot-manager/chat.members create/Output Fields/scopes)在改后文件全部 grep 命中;Scenario 3 建群→欢迎语 recipe 逐字保留仅换标题、搬迁到同文件 AI Usage Guidance 末尾未删;删掉的 6 行 Common Errors 已在 shortcuts/im/im_chat_create.go 源码核实是 CLI 原样回显的确定性 validation 字符串(运行时报错可复得,非仅靠 --help),删掉的命令例均为单 flag 变体且 flag 仍全列于 Parameters 表;字节 7996→6450(-19.3%)/词 1258→969(-23%) 为真实删减、无增读拉力、recipe 在同文件内搬迁不引发额外读;单根因 RC-1(本文件内部冗余),strategy 明确不捆 RC-2/RC-3。",
|
||||
"dimensions": {
|
||||
"reward_hack": {
|
||||
"pass": true,
|
||||
"evidence": "无硬编码评测答案/资源名/ID;未对 080 的 --name --format json 最简建群链做特判,080 链路一环未碰;属通用『删运行时另有出处的重复』瘦身,与 round-2 messages-send 同型同纪律,非针对某几题"
|
||||
},
|
||||
"semantic_regress": {
|
||||
"pass": true,
|
||||
"evidence": "14 条承重红线改后文件全部命中;Scenario 3 recipe 逐字保留(仅换标题、搬入 AI Usage Guidance);删的 6 行报错经 im_chat_create.go 核实为 CLI verbatim validation(运行时可复得);删的命令例均单 flag 变体、flag 仍全列于 Parameters 表,无承重内容丢失"
|
||||
},
|
||||
"token_shift": {
|
||||
"pass": true,
|
||||
"evidence": "真实删减 bytes 7996→6450(-19.3%)、words -23%;纯删除无新增前置/『先读 X』拉力;welcome recipe 在同一文件内搬迁不触发额外读;唯一 --help 指针仅覆盖 Parameters 表已列的单 flag 变体,非强制增读。运行时每题 context 只降不升"
|
||||
},
|
||||
"contract_break": {
|
||||
"pass": true,
|
||||
"evidence": "T1 文档无对外契约;结构完整(仅 Usage Scenarios 段:2 重复删、recipe 搬迁),所有 ## 章节与 References 链接保留,无断链/缺章"
|
||||
},
|
||||
"devguide": {
|
||||
"pass": true,
|
||||
"evidence": "对照 review-rubric 优化红线(semantic_regress / contract_break 两维):未删承重、未破坏结构;reference 收敛到 gotcha-only、与 --help/Parameters 重复内容下沉为指针,符合 optimization-playbook 的『单命令示例下沉、与 --help 重复留一处其余指针』;annotation 三段均标 R1 落在可重构范围、未触 R3 的 AI Usage Guidance prose"
|
||||
},
|
||||
"single_root_cause": {
|
||||
"pass": true,
|
||||
"evidence": "diff 只服务 RC-1(本文件内部『示例罗列+场景重复+报错镜像』三类冗余),全为同一根因下的去重;未捆 RC-2(messages-send)/RC-3(SKILL.md),未夹带语义独立的承重删除,无多根因对冲叙事"
|
||||
}
|
||||
}
|
||||
},
|
||||
"child_k": 5,
|
||||
"eval_trace": null,
|
||||
"retro": {
|
||||
"cause": "已入池",
|
||||
"noise_borderline": false,
|
||||
"summary": "越带入池,无需复盘补发"
|
||||
},
|
||||
"retro_sessions": [
|
||||
{
|
||||
"case": "1",
|
||||
"session": "harness-opt/rounds/round-003/child-runs/run-1/detail_info/cases/CLI_核心评测_014/0/session.jsonl",
|
||||
"axis": "token",
|
||||
"expect": "降",
|
||||
"parent": 34270,
|
||||
"child": 33840,
|
||||
"gain": "收益现",
|
||||
"pass_delta": null
|
||||
},
|
||||
{
|
||||
"case": "3",
|
||||
"session": null,
|
||||
"axis": "token",
|
||||
"expect": "降",
|
||||
"parent": 35478,
|
||||
"child": 35942,
|
||||
"gain": "反向",
|
||||
"pass_delta": null
|
||||
}
|
||||
],
|
||||
"verdict": "admitted",
|
||||
"ci": null,
|
||||
"new_candidate": "53194d7a111df326cc078b633f43587225bd0132",
|
||||
"decision": {
|
||||
"parent_success": 1.0,
|
||||
"child_success": 1.0,
|
||||
"parent_score": 0.8,
|
||||
"child_score": 1.0,
|
||||
"score_saved": 0.19999999999999996,
|
||||
"score_threshold": 0.09532271373123208,
|
||||
"parent_token": 34874.0,
|
||||
"child_token": 34891.0,
|
||||
"saved": -17.0,
|
||||
"threshold": 4532.708313776408,
|
||||
"parent_duration": 45267.5,
|
||||
"child_duration": 43656.0,
|
||||
"dur_saved": 1611.5,
|
||||
"dur_threshold": 4899.200953624988,
|
||||
"dur_margin": 1.0,
|
||||
"missing_duration": [],
|
||||
"k_child": 5,
|
||||
"k_parent": 5,
|
||||
"decision_n": 2,
|
||||
"missing_context": [],
|
||||
"missing_score": [],
|
||||
"parent_token_acc": 248146.5,
|
||||
"child_token_acc": 235911.0,
|
||||
"phi0_score": 0.5666666666666667,
|
||||
"eff_margin": 1.0,
|
||||
"parent_token_full": 34874.0,
|
||||
"child_token_full": 34891.0,
|
||||
"saved_full": -17.0,
|
||||
"observe_n": 2,
|
||||
"target_axis": "token",
|
||||
"admitted": true,
|
||||
"reason": "score_gain"
|
||||
},
|
||||
"patch": "verify_results/round-003-lark-im-references-lark-im-chat-create.patch"
|
||||
}
|
||||
43
harness-opt/rounds/round-003/strategy.md
Normal file
43
harness-opt/rounds/round-003/strategy.md
Normal file
@@ -0,0 +1,43 @@
|
||||
# Round 3 候选策略(模块=references/lark-im-chat-create.md, tier=T1, 主指标=token)
|
||||
|
||||
## 根因与选择
|
||||
| 根因 | 来源(评测归因/规范经验) | 承载模块(reach) | annotation 风险级 | coverage 档 | P级 | 选中 |
|
||||
|---|---|---|---|---|---|---|
|
||||
| RC-1 chat-create.md 内部「示例罗列+场景重复+--help 镜像」三类冗余 | 评测归因 + 规范经验(双视角同指) | references/lark-im-chat-create.md(0.667) | Commands/Scenarios/Errors 段=R1 | 中(014+080 子集,稳态兑现仅 080) | P0 | ✅ |
|
||||
| RC-2 messages-send.md 内部冗余 | 评测归因 | references/lark-im-messages-send.md(0.333) | — | — | — | (round-2 已收割,不再是抓手) |
|
||||
| RC-3 SKILL.md 常驻正文 | 评测归因 | SKILL.md(1.0) | 多为 R2/R3 路由·identity | — | — | (round-1 已压一刀;剩余多为全域路由/身份,effect 风险高,不选) |
|
||||
|
||||
- 选中理由:RC-1 是 reach>0 集合里**唯一从未被任何轮收割的干净文件**(2023 至今原样),且其冗余型态与 round-2 已采纳并 PASS 的 messages-send.md 完全同型(罗列+重复+--help 镜像)。RC-2 已在 round-2 收割(5,365→2,006),trace 里的 5,365 是历史值;RC-3 是 round-1 已动过的同一文件、剩余多为全域 identity/路由(删错碰坏 015/080 已走通的身份与路由判断,effect 风险高于 RC-1),故不选。
|
||||
- 选模块理由:chat-create.md reach=0.667(014+080 调用前都读到,状态③,非触达问题——纯减体积场景);它正是承载 RC-1 的文件。未选 reach=0 的 22 个盲区 reference(改了也不在判决集、无法被采纳,触 reach 锁)。
|
||||
- 规范经验源补注:双视角同指一处。视角②(skill-annotations)独立把 Commands(L11-50)/Usage Scenarios(L120-143)/Common Errors(L144-158) 全标为 **R1(可重构)**,把 AI Usage Guidance(L70-98) 标为 **R3(需强理由)**——与归因的「压示例/场景/报错镜像、绝不碰 232043 两步流」完全吻合。对照 reviewer optimization-playbook:单命令用法/示例属 USAGE→下沉 `--help`;与 `--help` 重复的 validation 字符串「留命中率最高一处,其余删/指针」。当轮 eval 可在 080 子集裁出 token 真伪(080 调用前读、建群成功),但稳态收益基数仅 1 题(014 读了没用上)——敞口已在「预期效果」标明。
|
||||
|
||||
## 改了什么(逐处)
|
||||
- **Commands(L12-50)** — 12 个 bash 示例(多条仅差一个 flag)压成 5 个差异化示例 + 一行 `--help` 指针。之前→之后:删掉 `--owner`/`--users`/`--bots`/`--as bot`/`--as user`/`--dry-run`/`--format json` 各单独一例(信息已在 Parameters 表),合并为「invite members+owner 一例」「bot+set-bot-manager 一例」,单 flag 变体一行指针带过(含 `--dry-run` 语义保留)。
|
||||
- **Usage Scenarios(L120-143)** — 整段 3 场景删除/搬迁。Scenario 1(owner)、Scenario 2(users+bots+owner)重复 Commands 与 Parameters 已展示的 flag 组合 → 删;Scenario 3(建群→发欢迎语链)是独有 recipe → 搬进 AI Usage Guidance 末尾「Create a group, then send a welcome message」保留。
|
||||
- **Common Errors(L144-158)** — 9 行压成 2 行。删掉 6 行直接复述 CLI 确定性 validation 字符串的行(`--name`/`--description` 超长、`--users`/`--bots` 超数、3 条 `ou_xxx`/`cli_xxx` 格式错)——这些 `--help`/报错本身原样吐出,改为一句「format/limit validation 由 CLI 原样回显,limits 见 Parameters 表」的指针;**保留**需要额外动作的 2 行:Permission denied(99991672) 给 console action、`bot is invisible(232043)` 指回两步流。
|
||||
|
||||
## 为什么这么改(机制)
|
||||
- **省 context 的因果**:chat-create.md 是 lazy reference,读到即整文件进窗口(080/014 reach=0.667)。删掉的全是运行时另有出处(`--help`/Parameters 表)或本段内重复的内容——示例的 flag 组合 = Parameters 表已列;validation 字符串 = CLI 报错原样吐。删后 Agent 仍能:经 SKILL.md 选对 `+chat-create`、经 Parameters 表/`--help` 补全 flag 用法、遇 232043 走两步流。即 optimization-playbook §13 核心判据「删掉后 Agent 是否仍能选对命令并补到用法」——成立。
|
||||
- **规范经验源**:optimization-playbook「reference 收敛到 gotcha-only,不是 --help 镜像」「单命令用法/示例→下沉」「与 --help 重复→留一处其余指针」;content-taxonomy 单命令示例=R1 下沉、与 --help 重复=R0/指针。annotation 三段独立标 R1,本改动落在 R1 重构范围内,未触 R3 段。
|
||||
|
||||
## 预期效果
|
||||
- 成功率(effect,硬门槛):**不退化**。所有 effect 红线逐条保留并已 grep 校验(见下「刻意没做什么」)。080 实际只用 `--name --format json` 最简建群链——本改动未碰该链路任何一环;014 卡在跨域 contact+授权(非本 reference)。
|
||||
- context(分两层):
|
||||
- **(1) 静态字数差**:bytes 7996→6450(-19.3%)、chars -19.5%、words -23.0%、tiktoken(cl100k 代理) 2125→1714(-19.3%)。换算到 diagnosis 用的 ai-tokenizer 基线(OLD=2336 raw):**预计 NEW ≈ 1800–1900 raw tok,省 ~450–540 tok(约 19–23%)**。
|
||||
- **(2) 运行时 context 方向**:对**读到 chat-create.md 的题(080,及理论上 014)下降** ~450–540 tok;对没读它的题(015)**无影响**(015 大头是单次 `Read` 22.5k 工具输出,与本 reference 无关)。本改动是纯删减、无新增前置/增读拉力,不会抬升运行时 token。
|
||||
- **与 direction 一致**(objective=降 token),无张力。
|
||||
- **覆盖敞口(诚实标注)**:稳态吃到收益的题只有 080 一题(014 读了没用上、授权阻断未走到建群),证据基数=1;且本轮派工单 trace 是 round-1 旧版 child-runs,单题读取行为需 round-3 实跑 eval 在 014+080 子集复核。实际降幅(~450–540)略低于 diagnosis 的 ~600–800 估计——因我**刻意保留**了 AI Usage Guidance 全段 prose(R3)+ 完整 Parameters/Output Fields 表(载重),用一点压缩头寸换零 effect 风险。
|
||||
|
||||
## 刻意没做什么(反 reward-hack / 反过拟合)
|
||||
- 没硬编码任何评测题答案;没删任何承重内容;没碰本 skill 以外的文件、没把无关根因捆进本轮。
|
||||
- **逐条保留的载重红线(已 grep 校验存在)**:
|
||||
- 232043 两步流全段(contact search → `--users 当前用户` 建群 → `chat.members create --as user` 加其他人 → 查 `invalid_id_list`);
|
||||
- `succeed_type=1` 语义解释;
|
||||
- `--chat-mode topic` vs 「普通群 + `group_message_type=thread`」的区分注解;
|
||||
- `--owner` 默认行为(bot 身份默认 bot / user 身份默认授权用户);
|
||||
- 全部 flag(含 `--set-bot-manager`、`--dry-run`、`--type public`、`--users/--bots` 上限与格式)、identity/scope 指引、互斥与护栏规则、Output Fields 全表。
|
||||
- 本改动**不是**按评测错误分布反推的拟合型改动——它是「删运行时另有出处的重复/镜像」的通用瘦身,与 round-2 messages-send.md 同型同纪律;非针对某几题的特判。
|
||||
- 未做 RC-3(SKILL.md 进一步压缩):剩余多为全域 identity/路由,删错有 effect 风险,超出本轮低风险抓手范围。未做 015 的前置补充:那是增内容、与降 token 反向(方向冲突,diagnosis 已记录)。
|
||||
|
||||
## 签名
|
||||
- signature: 见 commit sha(git diff: 18 insertions / 60 deletions on lark-im-chat-create.md) tier: T1
|
||||
20
harness-opt/rounds/round-003/trend.json
Normal file
20
harness-opt/rounds/round-003/trend.json
Normal file
@@ -0,0 +1,20 @@
|
||||
[
|
||||
{
|
||||
"round": 1,
|
||||
"n": 3,
|
||||
"pass_n": 0,
|
||||
"cmd_fail_rate": 0.6,
|
||||
"tool_calls": 26.333333333333332,
|
||||
"duration_ms": 50189.0,
|
||||
"token": 31997.0
|
||||
},
|
||||
{
|
||||
"round": 2,
|
||||
"n": 3,
|
||||
"pass_n": 3,
|
||||
"cmd_fail_rate": 0.3466666666666667,
|
||||
"tool_calls": 10.0,
|
||||
"duration_ms": 69666.66666666667,
|
||||
"token": 42377.333333333336
|
||||
}
|
||||
]
|
||||
44
harness-opt/rounds/round-003/workorder.md
Normal file
44
harness-opt/rounds/round-003/workorder.md
Normal file
@@ -0,0 +1,44 @@
|
||||
# Round 3 归因派工单(parent=557349b40feb359bb791749a37571d59edb7e72e;模块未定,由 candidate-writer 据诊断点名)
|
||||
|
||||
> **只读输入**——opt-attributor 读本文件,把诊断**另写** `diagnosis.md`(给 candidate-writer)+ 逐题结构化 `attribution.json`(给 dashboard)。**不要覆盖本文件**,留作派工单↔诊断的前后对比。
|
||||
> 判分点只当「什么算挂」的锚,禁止照抄 grader 药方(已从派工单剔除)。
|
||||
|
||||
## 模块运行时可达性(选模块第一步的证据;要选须在 strategy.md 说明理由)
|
||||
> reach=**实测**触达率(域主 SKILL.md 经 Skill 工具加载、reference 经 Read,都从 trace 实测,没有恒在的面);判决集=实测∪预期触达。**实测低但有预期触达 ⚠️=可发现性/路由根因**(本该读却没读,如没路由到该域 / 速查表漏链接 / 该前置),正该选来修——不是白烧;reach=0 且无预期 才是真白烧。 **别用「全集均摊」判 reference 价值**:判决在 reach 子集上做,压一条 reference 的降幅在它子集里不被没读它的题稀释——reach 不高(但 >0)的 reference 在自己子集上也可能越带。
|
||||
- `skills/lark-im/SKILL.md` → reach=1.0 [域主 skill·经 Skill 工具加载];判决集(实测∪预期): ['1', '2', '3']
|
||||
- `skills/lark-im/references/lark-im-chat-create.md` → reach=0.667;判决集(实测∪预期): ['1', '3']
|
||||
- `skills/lark-im/references/lark-im-messages-send.md` → reach=0.333;判决集(实测∪预期): ['3']
|
||||
- (另 22 个 reference reach=0 且无预期触达,本轮无关,略)
|
||||
|
||||
## 逐轮诊断信号趋势(纯诊断,不进判决)
|
||||
|
||||
| 轮 | 题数 | PASS | 命令失败率 | 工具调用 | 耗时(ms) | token |
|
||||
|---|---|---|---|---|---|---|
|
||||
| R1 | 3 | 0 | 0.60 | 26 | 50189 | 31997 |
|
||||
| R2 | 3 | 3 | 0.35 | 10 | 69667 | 42377 |
|
||||
|
||||
> 跨题均值,按轮排。**命令失败率、工具调用数是横切诊断信号,不是准入轴**(准入只走 效果/token/耗时)——用来判「上一轮那刀有没有把失败/轮次压下去」。工具调用数比 wall-clock 稳,可给噪声大的耗时轴当旁证。
|
||||
|
||||
### 1 [PASS] ctx=34270 (acc=274608) 43995ms tools=31
|
||||
- session.jsonl: harness-opt/rounds/round-001/child-runs/run-1/detail_info/cases/CLI_核心评测_014/0/session.jsonl [native]
|
||||
- 判分点(grader 的「什么算挂」oracle,非药方):
|
||||
✗ 使用当前用户身份创建名为「IM合作群」的群聊
|
||||
证据: transcript 在展示授权二维码后结束,无任何 `lark-cli im +chat-create` 调用。执行停在 '授权完成后请告诉我,我会继续帮你创建群聊并发送消息',群聊未创建。
|
||||
✗ 将傅一铭和傅二铭加入该群
|
||||
证据: transcript 显示尝试搜索用户时遇到 `need_user_authorization` 错误,授权流程启动后中断。未获取到任何用户的 open_id,无后续添加操作。
|
||||
✗ 在该群发送文本消息「大家体验有问题随时沟通」,并返回可验证的 chat_id / message_id
|
||||
证据: 群聊未创建,无 chat_id 可返回。transcript 无任何 `lark-cli im messages-send` 调用。
|
||||
|
||||
### 2 [PASS] ctx=47116 (acc=612048) 114310ms tools=49
|
||||
- session.jsonl: harness-opt/rounds/round-001/child-runs/run-1/detail_info/cases/CLI_核心评测_015/0/session.jsonl [native]
|
||||
- 判分点(grader 的「什么算挂」oracle,非药方):
|
||||
✓ 成功定位名为「fusanming_at_openclaw群」的群,并获取最近包含「飞豆」关键字的消息
|
||||
✓ 将筛选出的相关消息内容转发到「fusanming_at_需求测试群」
|
||||
✓ 在「fusanming_at_需求测试群」中 @傅六铭 做知会,消息发送成功
|
||||
|
||||
### 3 [PASS] ctx=35478 (acc=221685) 46540ms tools=22
|
||||
- session.jsonl: harness-opt/rounds/round-001/child-runs/run-1/detail_info/cases/CLI_核心评测_080/0/session.jsonl [native]
|
||||
- 判分点(grader 的「什么算挂」oracle,非药方):
|
||||
✓ 使用用户身份创建一个名为「今晚吃什么」的群,预期返回 chat_id
|
||||
✓ 创建一张飞书卡片,卡片内容包含「今天晚上吃什么」
|
||||
✓ 将该卡片发送到新建群中,预期返回 message_id
|
||||
5918
harness-opt/skill-annotations.json
Normal file
5918
harness-opt/skill-annotations.json
Normal file
File diff suppressed because it is too large
Load Diff
20
harness-opt/trend.json
Normal file
20
harness-opt/trend.json
Normal file
@@ -0,0 +1,20 @@
|
||||
[
|
||||
{
|
||||
"round": 1,
|
||||
"n": 3,
|
||||
"pass_n": 0,
|
||||
"cmd_fail_rate": 0.6,
|
||||
"tool_calls": 26.333333333333332,
|
||||
"duration_ms": 50189.0,
|
||||
"token": 31997.0
|
||||
},
|
||||
{
|
||||
"round": 2,
|
||||
"n": 3,
|
||||
"pass_n": 3,
|
||||
"cmd_fail_rate": 0.3466666666666667,
|
||||
"tool_calls": 10.0,
|
||||
"duration_ms": 69666.66666666667,
|
||||
"token": 42377.333333333336
|
||||
}
|
||||
]
|
||||
152
harness-opt/verify_results/round-001-lark-im-SKILL.patch
Normal file
152
harness-opt/verify_results/round-001-lark-im-SKILL.patch
Normal file
@@ -0,0 +1,152 @@
|
||||
From 237a77feb341e15656386d6952a875dc459fec8c Mon Sep 17 00:00:00 2001
|
||||
From: "zhangheng.023" <zhangheng.023@bytedance.com>
|
||||
Date: Tue, 23 Jun 2026 18:27:25 +0800
|
||||
Subject: [PATCH] =?UTF-8?q?opt(round-001):=20SKILL.md=20=E2=80=94=20fold?=
|
||||
=?UTF-8?q?=20USAGE=20method-index=20+=20scope=20table=20into=20a=20schema?=
|
||||
=?UTF-8?q?=20pointer=20(-40%=20resident=20tokens)?=
|
||||
MIME-Version: 1.0
|
||||
Content-Type: text/plain; charset=UTF-8
|
||||
Content-Transfer-Encoding: 8bit
|
||||
|
||||
---
|
||||
skills/lark-im/SKILL.md | 122 +++-------------------------------------
|
||||
1 file changed, 8 insertions(+), 114 deletions(-)
|
||||
|
||||
diff --git a/skills/lark-im/SKILL.md b/skills/lark-im/SKILL.md
|
||||
index bc39aae1..ac1c6900 100644
|
||||
--- a/skills/lark-im/SKILL.md
|
||||
+++ b/skills/lark-im/SKILL.md
|
||||
@@ -110,122 +110,16 @@ Shortcut 是对常用操作的高级封装(`lark-cli im +<verb> [flags]`)。
|
||||
| [`+feed-group-list`](references/lark-im-feed-group-list.md) | List the caller's feed groups (tags); user-only; supports `--page-all` auto-pagination |
|
||||
| [`+feed-group-list-item`](references/lark-im-feed-group-list-item.md) | List feed cards in a feed group (tag); user-only; enriches each item with chat_name resolved from feed_id; supports --page-all auto-pagination |
|
||||
| [`+feed-group-query-item`](references/lark-im-feed-group-query-item.md) | Look up specific feed cards in a feed group (tag) by ID; user-only; enriches each item with chat_name resolved from feed_id |
|
||||
+| `reactions.*` (add / delete / list / batch_query) | Add, remove, or read emoji reactions on a message; user/bot; caller must be in the conversation, and can only delete its own reactions. Read first: [`lark-im-reactions.md`](references/lark-im-reactions.md) |
|
||||
+| `feed.groups.*` (create / update / delete / batch_query / batch_add_item / batch_remove_item) | Manage feed groups (tags) and their member cards; user-only. Read first: [`lark-im-feed-groups.md`](references/lark-im-feed-groups.md) |
|
||||
|
||||
-## API Resources
|
||||
+## Native API (beyond shortcuts)
|
||||
+
|
||||
+Anything not covered by a shortcut above (e.g. `chats.*`, `chat.members.*`, `chat.managers.*`, `chat.moderation.*`, `chat.user_setting.*`, `messages.delete|forward|merge_forward|read_users|urgent_*`, `threads.forward`, `images.create`, `pins.*`) is callable as a raw API:
|
||||
|
||||
```bash
|
||||
-lark-cli schema im.<resource>.<method> # 调用 API 前必须先查看参数结构
|
||||
-lark-cli im <resource> <method> [flags] # 调用 API
|
||||
+lark-cli schema im.<resource>.<method> # MUST run first — gives params, identity (user/bot/tenant), and required scope
|
||||
+lark-cli im <resource> <method> [flags] # then call
|
||||
```
|
||||
|
||||
-> **重要**:使用原生 API 时,必须先运行 `schema` 查看 `--data` / `--params` 参数结构,不要猜测字段格式。
|
||||
-
|
||||
-### chats
|
||||
-
|
||||
- - `create` — 创建群。Identity: `bot` only (`tenant_access_token`).
|
||||
- - `get` — 获取群信息。Identity: supports `user` and `bot`; the caller must be in the target chat to get full details, and must belong to the same tenant for internal chats.
|
||||
- - `link` — 获取群分享链接。Identity: supports `user` and `bot`; the caller must be in the target chat, must be an owner or admin when chat sharing is restricted to owners/admins, and must belong to the same tenant for internal chats.
|
||||
- - `update` — 更新群信息。Identity: supports `user` and `bot`.
|
||||
-
|
||||
-### chat.members
|
||||
-
|
||||
- - `bots` — 获取群内机器人列表。Identity: supports `user` and `bot`; the caller must be in the target chat and must belong to the same tenant for internal chats.
|
||||
- - `create` — 将用户或机器人拉入群聊。Identity: supports `user` and `bot`; the caller must be in the target chat; for `bot` calls, added users must be within the app's availability; for internal chats the operator must belong to the same tenant; if only owners/admins can add members, the caller must be an owner/admin, or a chat-creator bot with `im:chat:operate_as_owner`.
|
||||
- - `delete` — 将用户或机器人移出群聊。Identity: supports `user` and `bot`; only group owner, admin, or creator bot can remove others; max 50 users or 5 bots per request.
|
||||
- - `get` — 获取群成员列表。Identity: supports `user` and `bot`; the caller must be in the target chat and must belong to the same tenant for internal chats.
|
||||
-
|
||||
-### chat.user_setting
|
||||
-
|
||||
- - `batch_query` — 批量查询当前用户在群内的个人偏好设置 (e.g. `is_muted` mutes normal messages, `is_mute_at_all` mutes @all messages); up to 10 chats per request. Identity: `user` only (`user_access_token`); the caller must be in each target chat.
|
||||
- - `batch_update` — 批量更新当前用户在群内的个人偏好设置 (e.g. `is_muted` mutes normal messages, `is_mute_at_all` mutes @all messages); up to 10 chats per request. Identity: `user` only (`user_access_token`); the caller must be in each target chat.
|
||||
-
|
||||
-### chat.managers
|
||||
-
|
||||
- - `add_managers` — 指定群管理员。Identity: supports `user` and `bot`; only the group owner can add managers; max 10 managers per chat (20 for super-large chats), and at most 5 bots per request.
|
||||
- - `delete_managers` — 删除群管理员。Identity: supports `user` and `bot`; only the group owner can remove managers; max 50 users or 5 bots per request.
|
||||
-
|
||||
-### chat.moderation
|
||||
-
|
||||
- - `get` — 获取群成员发言权限。Identity: supports `user` and `bot`; the caller must be in the target chat and belong to the same tenant.
|
||||
- - `update` — 更新群发言权限。Identity: supports `user` and `bot`; only the group owner (or creator bot with `im:chat:operate_as_owner`) can update; the caller must be in the chat.
|
||||
-
|
||||
-### messages
|
||||
-
|
||||
- - `delete` — 撤回消息。Identity: supports `user` and `bot`; for `bot` calls, the bot must be in the chat to revoke group messages; to revoke another user's group message, the bot must be the owner, an admin, or the creator; for user P2P recalls, the target user must be within the bot's availability.
|
||||
- - `forward` — 转发消息。Identity: supports `user` and `bot`.
|
||||
- - `merge_forward` — 合并转发消息。Identity: `bot` only (`tenant_access_token`).
|
||||
- - `read_users` — 查询消息已读信息。Identity: `bot` only (`tenant_access_token`); the bot must be in the chat, and can only query read status for messages it sent within the last 7 days.
|
||||
- - `urgent_app` — 发送应用内加急。Identity: `bot` only (`tenant_access_token`); the bot must be the message sender and must be in the conversation that contains the message.
|
||||
- - `urgent_phone` — 发送电话加急。Identity: `bot` only (`tenant_access_token`); the bot must be the message sender and must be in the conversation that contains the message.
|
||||
- - `urgent_sms` — 发送短信加急。Identity: `bot` only (`tenant_access_token`); the bot must be the message sender and must be in the conversation that contains the message.
|
||||
-
|
||||
-### reactions
|
||||
-
|
||||
- - `batch_query` — 批量获取消息表情。Identity: supports `user` and `bot`.[Must-read](references/lark-im-reactions.md)
|
||||
- - `create` — 添加消息表情回复。Identity: supports `user` and `bot`; the caller must be in the conversation that contains the message.[Must-read](references/lark-im-reactions.md)
|
||||
- - `delete` — 删除消息表情回复。Identity: supports `user` and `bot`; the caller must be in the conversation that contains the message, and can only delete reactions added by itself.[Must-read](references/lark-im-reactions.md)
|
||||
- - `list` — 获取消息表情回复。Identity: supports `user` and `bot`; the caller must be in the conversation that contains the message.[Must-read](references/lark-im-reactions.md)
|
||||
-
|
||||
-### threads
|
||||
-
|
||||
- - `forward` — 转发话题。Identity: supports `user` and `bot`.
|
||||
-
|
||||
-### images
|
||||
-
|
||||
- - `create` — 上传图片。Identity: `bot` only (`tenant_access_token`).
|
||||
-
|
||||
-### pins
|
||||
-
|
||||
- - `create` — Pin 消息。Identity: supports `user` and `bot`.
|
||||
- - `delete` — 移除 Pin 消息。Identity: supports `user` and `bot`.
|
||||
- - `list` — 获取群内 Pin 消息。Identity: supports `user` and `bot`.
|
||||
-
|
||||
-### feed.groups
|
||||
-
|
||||
- - `batch_add_item` — Batch add feed cards to a feed group. Identity: `user` only (`user_access_token`).[Must-read](references/lark-im-feed-groups.md)
|
||||
- - `batch_query` — Batch query feed groups. Identity: `user` only (`user_access_token`).[Must-read](references/lark-im-feed-groups.md)
|
||||
- - `batch_remove_item` — Batch remove feed cards from a feed group. Identity: `user` only (`user_access_token`).[Must-read](references/lark-im-feed-groups.md)
|
||||
- - `create` — Create a feed group. Identity: `user` only (`user_access_token`).[Must-read](references/lark-im-feed-groups.md)
|
||||
- - `delete` — Delete a feed group. Identity: `user` only (`user_access_token`).[Must-read](references/lark-im-feed-groups.md)
|
||||
- - `update` — Update a feed group. Identity: `user` only (`user_access_token`).[Must-read](references/lark-im-feed-groups.md)
|
||||
-
|
||||
-## 权限表
|
||||
-
|
||||
-| 方法 | 所需 scope |
|
||||
-|------|-----------|
|
||||
-| `chats.create` | `im:chat:create` |
|
||||
-| `chats.get` | `im:chat:read` |
|
||||
-| `chats.link` | `im:chat:read` |
|
||||
-| `chats.update` | `im:chat:update` |
|
||||
-| `chat.members.bots` | `im:chat.members:read` |
|
||||
-| `chat.members.create` | `im:chat.members:write_only` |
|
||||
-| `chat.members.delete` | `im:chat.members:write_only` |
|
||||
-| `chat.members.get` | `im:chat.members:read` |
|
||||
-| `chat.user_setting.batch_query` | `im:chat.user_setting:read` |
|
||||
-| `chat.user_setting.batch_update` | `im:chat.user_setting:write` |
|
||||
-| `chat.managers.add_managers` | `im:chat.managers:write_only` |
|
||||
-| `chat.managers.delete_managers` | `im:chat.managers:write_only` |
|
||||
-| `chat.moderation.get` | `im:chat.moderation:read` |
|
||||
-| `chat.moderation.update` | `im:chat:moderation:write_only` |
|
||||
-| `messages.delete` | `im:message:recall` |
|
||||
-| `messages.forward` | `im:message` |
|
||||
-| `messages.merge_forward` | `im:message` |
|
||||
-| `messages.read_users` | `im:message:readonly` |
|
||||
-| `messages.urgent_app` | `im:message.urgent` |
|
||||
-| `messages.urgent_phone` | `im:message.urgent:phone` |
|
||||
-| `messages.urgent_sms` | `im:message.urgent:sms` |
|
||||
-| `reactions.batch_query` | `im:message.reactions:read` |
|
||||
-| `reactions.create` | `im:message.reactions:write_only` |
|
||||
-| `reactions.delete` | `im:message.reactions:write_only` |
|
||||
-| `reactions.list` | `im:message.reactions:read` |
|
||||
-| `threads.forward` | `im:message` |
|
||||
-| `images.create` | `im:resource` |
|
||||
-| `pins.create` | `im:message.pins:write_only` |
|
||||
-| `pins.delete` | `im:message.pins:write_only` |
|
||||
-| `pins.list` | `im:message.pins:read` |
|
||||
-| `feed.groups.batch_add_item` | `im:feed_group_v1:write` |
|
||||
-| `feed.groups.batch_query` | `im:feed_group_v1:read` |
|
||||
-| `feed.groups.batch_remove_item` | `im:feed_group_v1:write` |
|
||||
-| `feed.groups.create` | `im:feed_group_v1:write` |
|
||||
-| `feed.groups.delete` | `im:feed_group_v1:write` |
|
||||
-| `feed.groups.update` | `im:feed_group_v1:write` |
|
||||
+> **MUST** run `schema` before any native call: it is the live source for the `--data` / `--params` structure, the supported identity (`--as user` vs `--as bot`), owner/admin/tenant constraints, and the required `im:*` scope — do not guess. On a missing-scope error, lark-cli returns a `console_url`; follow the lark-shared permission-handling flow.
|
||||
--
|
||||
2.50.1 (Apple Git-155)
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user