From b67bd33fb7bb255e936b156d58623c220aae5b5f Mon Sep 17 00:00:00 2001 From: whzy <2402686765@qq.com> Date: Fri, 26 Jun 2026 21:09:12 +0800 Subject: [PATCH] feat(feishu): validate real exports and zh-CN output --- docs/FEISHU_ENVIRONMENT.md | 16 +- docs/FEISHU_GITLINK_REDESIGN_RESEARCH.md | 19 +- docs/FEISHU_OPENAPI_INVENTORY.md | 582 ++++++++++++++++++ docs/PR_VISUAL_GUIDE.md | 10 +- ...EISHU_API_COLLECTION_CHECKLIST_20260626.md | 192 ++++++ reports/FEISHU_PERMISSION_MATRIX.md | 18 +- reports/FEISHU_SMOKE_20260626.md | 218 +++++-- .../FEISHU_USER_COLLECTION_LIST_20260626.md | 175 ++++++ shortcuts/feishu/bitable_sync.go | 26 +- shortcuts/feishu/card.go | 43 +- shortcuts/feishu/digest.go | 141 +++-- shortcuts/feishu/doc_export.go | 20 +- shortcuts/feishu/feishu.go | 10 +- shortcuts/feishu/feishu_test.go | 50 ++ shortcuts/feishu/l10n.go | 181 ++++++ shortcuts/feishu/task.go | 52 +- 16 files changed, 1597 insertions(+), 156 deletions(-) create mode 100644 docs/FEISHU_OPENAPI_INVENTORY.md create mode 100644 reports/FEISHU_API_COLLECTION_CHECKLIST_20260626.md create mode 100644 reports/FEISHU_USER_COLLECTION_LIST_20260626.md create mode 100644 shortcuts/feishu/l10n.go diff --git a/docs/FEISHU_ENVIRONMENT.md b/docs/FEISHU_ENVIRONMENT.md index eeff1c5..b04fe31 100644 --- a/docs/FEISHU_ENVIRONMENT.md +++ b/docs/FEISHU_ENVIRONMENT.md @@ -59,6 +59,7 @@ $env:FEISHU_APP_SECRET="REDACTED" | `FEISHU_WIKI_URL` | Existing Wiki page URL | Optional target | `+doc-export` | Can expose workspace/resource ID | Copy from Feishu Wiki | | `FEISHU_WIKI_NODE_TOKEN` | Existing Wiki node token | Optional target | `+doc-export` | Yes | Parsed from Wiki URL or API | | `FEISHU_FOLDER_TOKEN` | Folder token for creating a new DocX | Optional target | `+doc-export` | Yes | Feishu Drive folder URL / Open Platform docs | +| `FEISHU_DOCUMENT_ID` | Existing DocX document ID for append | Optional target | `+doc-export` | Yes | Existing Feishu DocX URL or Open Platform docs | Legacy compatibility: @@ -71,6 +72,15 @@ Example: ```powershell $env:FEISHU_WIKI_URL="https://example.feishu.cn/wiki/REDACTED" $env:FEISHU_FOLDER_TOKEN="REDACTED" +$env:FEISHU_DOCUMENT_ID="REDACTED" +``` + +For localized output, generate the source workflow report and the Feishu output +with the same language flag: + +```powershell +go run . workflow +repo-report --owner "$env:GITLINK_OWNER" --repo "$env:GITLINK_REPO" --lang zh-CN --format json > .local\report.zh-CN.json +go run . feishu +notify --from-workflow-json .local\report.zh-CN.json --lang zh-CN --format table ``` ## Base / Bitable Variables @@ -106,8 +116,10 @@ Current limitation: ```text The experimental task create command creates task candidates through the Task API. -Project/section placement may require additional Feishu Task identifiers and scopes. -If placement fails, record the Open Platform error in the smoke report. +Task project and section IDs are currently collected and redacted in output, +but they are not yet mapped into the create-task request body. +Project/section placement should be wired only after the official request fields +and test-enterprise behavior are confirmed. ``` ## GitLink Test Variables diff --git a/docs/FEISHU_GITLINK_REDESIGN_RESEARCH.md b/docs/FEISHU_GITLINK_REDESIGN_RESEARCH.md index ea9feb9..e4dade1 100644 --- a/docs/FEISHU_GITLINK_REDESIGN_RESEARCH.md +++ b/docs/FEISHU_GITLINK_REDESIGN_RESEARCH.md @@ -226,16 +226,22 @@ Observed real Feishu test: custom bot send: passed notify send: passed weekly-report send: passed +owner-digest send: passed +contributor-digest send: passed Bitable schema / records dry-run: passed tenant_access_token: acquired -Wiki node: resolved -DocX write: blocked by Feishu 403 / 1770032 / forBidden +DocX append: passed after the app and target document had permission +Bitable search/create/update: passed after the test table fields were created +Task create: passed at minimal summary/description level +zh-CN Feishu output: passed for cards, digests, DocX blocks, and task candidates ``` Interpretation: ```text -The app credentials and Wiki read path can work, but document writes still require correct DocX / Drive scopes and target document or folder permissions. +The Open Platform path is practically usable in a configured test enterprise, +but it should stay experimental because resource-level scopes, table fields, +document permissions, and task placement are still operator-managed. ``` ### Current Bitable Usage @@ -1077,6 +1083,13 @@ docs/FEISHU_ACTION_GATEWAY_SECURITY.md docs/FEISHU_LARK_CLI_INTEROP.md ``` +Detailed API collection for the implemented branch is maintained in: + +```text +docs/FEISHU_OPENAPI_INVENTORY.md +reports/FEISHU_API_COLLECTION_CHECKLIST_20260626.md +``` + Recommended command planning documents: ```text diff --git a/docs/FEISHU_OPENAPI_INVENTORY.md b/docs/FEISHU_OPENAPI_INVENTORY.md new file mode 100644 index 0000000..14a96ab --- /dev/null +++ b/docs/FEISHU_OPENAPI_INVENTORY.md @@ -0,0 +1,582 @@ +# Feishu OpenAPI Inventory for GitLink CLI + +Date: 2026-06-26 + +This document maps the Feishu / Lark Open Platform APIs collected for the +`gitlink-cli feishu` integration to the current command surface and the next +implementation gaps. + +The inventory is intentionally split into three surfaces: + +```text +Layer 1: Stable custom bot export +Layer 2: Experimental Open Platform validation +Layer 3: Future callback-based GitLink action gateway +``` + +No implemented command in this branch performs GitLink write operations. + +## Source Index + +Official Feishu / Lark references used for this inventory: + +```text +Custom bot: +https://open.feishu.cn/document/client-docs/bot-v3/add-custom-bot +https://open.feishu.cn/document/feishu-cards/quick-start/send-message-cards-with-custom-bot?lang=zh-CN + +App authentication: +https://open.feishu.cn/document/server-docs/authentication-management/access-token/tenant_access_token_internal?lang=zh-CN + +IM app bot: +https://open.feishu.cn/document/server-docs/im-v1/message/create?lang=zh-CN + +DocX / Wiki: +https://open.feishu.cn/document/server-docs/docs/docs/docx-v1/document/create +https://open.feishu.cn/document/server-docs/docs/docs/docx-v1/document-block/create?lang=zh-CN +https://open.feishu.cn/document/server-docs/docs/wiki-v2/space/get_node + +Base / Bitable: +https://open.feishu.cn/document/server-docs/docs/bitable-v1/app-table-record/search +https://open.feishu.cn/document/server-docs/docs/bitable-v1/app-table-record/create?lang=zh-CN +https://open.feishu.cn/document/server-docs/docs/bitable-v1/app-table-record/update + +Task: +https://open.feishu.cn/document/task-v2/task/create?lang=zh-CN + +lark-cli: +https://github.com/larksuite/cli +https://open.larksuite.com/document/mcp_open_tools/feishu-cli-let-ai-actually-do-your-work-in-feishu +https://www.feishu.cn/feishu-cli +``` + +## Current API Usage Summary + +| Area | Endpoint / API family | Current command | Status | Write target | Notes | +| --- | --- | --- | --- | --- | --- | +| Custom bot webhook | `POST /open-apis/bot/v2/hook/{token}` | `+bot-test`, `+notify`, `+weekly-report`, `+owner-digest`, `+contributor-digest` | Implemented stable | Feishu chat message | Requires `--send`; preview by default | +| Custom bot signature | timestamp + HMAC-SHA256 signing secret | same as above | Implemented stable | Request signature only | `FEISHU_WEBHOOK_SECRET` optional | +| Tenant token | `POST /auth/v3/tenant_access_token/internal` | `+doc-export`, `+bitable-sync`, `+task-create` | Implemented experimental | Tenant token | No token cache yet | +| Wiki node resolution | `GET /wiki/v2/spaces/get_node?token=...` | `+doc-export` | Implemented experimental | Wiki metadata read | Used to resolve Wiki node to DocX object token | +| DocX create | `POST /docx/v1/documents` | `+doc-export` | Implemented experimental | New DocX document | Requires folder/resource permission | +| DocX append blocks | `POST /docx/v1/documents/{document_id}/blocks/{block_id}/children` | `+doc-export` | Implemented experimental | DocX block tree | Real write can fail on scope or document permission | +| Bitable search | `POST /bitable/v1/apps/{app_token}/tables/{table_id}/records/search` | `+bitable-sync` | Implemented experimental | Existing Base table | Searches by `unique_key` field | +| Bitable create record | `POST /bitable/v1/apps/{app_token}/tables/{table_id}/records` | `+bitable-sync` | Implemented experimental | Existing Base table | No table/field/view creation | +| Bitable update record | `PUT /bitable/v1/apps/{app_token}/tables/{table_id}/records/{record_id}` | `+bitable-sync` | Implemented experimental | Existing Base table | Never deletes records | +| Task create | `POST /task/v2/tasks` | `+task-create` | Implemented experimental | Feishu task | Project/section placement is not mapped into request body yet | +| IM app bot send | `POST /im/v1/messages?receive_id_type=...` | none | Planned | App-bot message | Needed for direct/group app bot sends beyond custom bot | +| Card callbacks | Interactive card callback / event subscription | none | Future | Callback server | Required before Feishu-triggered GitLink actions | +| User identity | open_id / union_id / user lookup | none | Future | Identity mapping | Required before personalized contributor routing | + +## Layer 1: Stable Custom Bot Export + +### Implemented APIs + +#### Custom Bot Webhook + +Current commands: + +```text ++bot-test ++notify ++weekly-report ++owner-digest ++contributor-digest +``` + +Inputs: + +```text +FEISHU_WEBHOOK_URL +FEISHU_WEBHOOK_SECRET optional +--send required for real delivery +--dry-run conflicts with --send +``` + +Current behavior: + +```text +Builds Feishu interactive card payloads. +Signs webhook requests when a secret is configured. +Prints local previews by default. +Redacts webhook URLs and secrets from normal output. +Only includes navigation buttons. +``` + +Limits: + +```text +No personalized routing. +No app-level chat_id. +No callback execution. +No Feishu resource write. +No GitLink resource write. +``` + +Next hardening: + +```text +Add more card color/stage variants for PR review state. +Add compact owner card and detailed digest variants. +Add screenshot-backed smoke evidence after real webhook env is restored. +``` + +## Layer 2: Experimental Open Platform Validation + +### App Authentication + +Endpoint: + +```text +POST /auth/v3/tenant_access_token/internal +``` + +Current commands: + +```text ++doc-export ++bitable-sync ++task-create +``` + +Inputs: + +```text +FEISHU_APP_ID +FEISHU_APP_SECRET +``` + +Current behavior: + +```text +Fetches tenant_access_token before Open Platform writes. +Does not persist or cache tenant_access_token. +Does not print the raw token. +``` + +Next hardening: + +```text +Add +app-check. +Cache token in memory during one command execution only. +Add scope diagnostics where official scope names are confirmed. +``` + +### DocX / Wiki + +Endpoints: + +```text +GET /wiki/v2/spaces/get_node?token=... +POST /docx/v1/documents +POST /docx/v1/documents/{document_id}/blocks/{parent_block_id}/children +``` + +Current command: + +```text ++doc-export +``` + +Inputs: + +```text +FEISHU_APP_ID +FEISHU_APP_SECRET +FEISHU_WIKI_URL or FEISHU_WIKI_NODE_TOKEN +FEISHU_FOLDER_TOKEN optional +FEISHU_DOCUMENT_ID optional +--send required for real write +``` + +Current behavior: + +```text +Preview renders workflow report content locally. +Wiki URL can be parsed into a node token. +Wiki node can be resolved to a DocX object token. +Existing DocX / Wiki target is appended when allowed. +Folder token can be used to create a new DocX when allowed. +Diagnostics preserve Feishu errors without leaking tokens. +``` + +Known blockers: + +```text +The app must have approved document scopes. +The app must be able to edit the target Wiki / DocX page. +For folder creation, the app must be able to create files in the target folder. +The command does not modify document permissions. +``` + +Local UI observation: + +```text +The Feishu desktop app currently shows a cloud-doc permission request flow. +This supports the current design decision that resource-level document access +must be handled by the owner/admin outside the CLI. +``` + +Next hardening: + +```text +Add +app-check diagnostics for DocX/Wiki scopes. +Add clearer output for target type: wiki node, existing doc, folder creation. +Add optional markdown-only export for manual paste into Feishu Docs. +``` + +### Base / Bitable + +Endpoints: + +```text +POST /bitable/v1/apps/{app_token}/tables/{table_id}/records/search +POST /bitable/v1/apps/{app_token}/tables/{table_id}/records +PUT /bitable/v1/apps/{app_token}/tables/{table_id}/records/{record_id} +``` + +Current commands: + +```text ++bitable-schema ++bitable-records ++bitable-sync +``` + +Inputs: + +```text +FEISHU_APP_ID +FEISHU_APP_SECRET +FEISHU_BASE_APP_TOKEN +FEISHU_REPORT_TABLE_ID +FEISHU_ISSUE_TABLE_ID +FEISHU_PR_TABLE_ID +FEISHU_CONTRIBUTOR_TABLE_ID optional +FEISHU_TASK_TABLE_ID optional +--send required for real sync +``` + +Current tables: + +```text +reports +issues +prs +contributors +tasks +``` + +Current behavior: + +```text ++bitable-schema outputs a dry-run schema. ++bitable-records outputs summary-oriented local records. ++bitable-sync previews by default. ++bitable-sync --send searches by unique_key, updates if found, creates if missing. +If search fails, the command falls back to create-only for that record. +Slice values are flattened into newline-separated text before OpenAPI writes. +No records are deleted. +``` + +Local test-enterprise validation on 2026-06-26: + +```text +The provided Base links resolved to one Base and one table with multiple views. +The table initially contained only default fields. +The missing fields were created manually through OpenAPI for validation. ++bitable-sync then successfully created and updated reports, issues, prs, +contributors, and task records in the test table. +``` + +Known blockers: + +```text +The Base app must already exist. +The target tables must already exist. +The target tables must contain a compatible unique_key field. +Field types must be compatible with generated record values. +No Bitable view creation is implemented. +No table/field creation is implemented. +Current records are summary buckets, not full row-level PR/Issue/CI records. +``` + +Next hardening: + +```text +Add table/field validation before writes. +Add row-level records for PRs, Issues, CI runs, milestones, releases, and audits. +Add optional Bitable view planning output for Kanban, Gantt, Calendar, Gallery, Form, and Dashboard. +Keep real view creation as a separate permissioned task. +``` + +### Task + +Endpoint: + +```text +POST /task/v2/tasks +``` + +Current commands: + +```text ++task-preview ++task-create +``` + +Inputs: + +```text +FEISHU_APP_ID +FEISHU_APP_SECRET +FEISHU_TASK_PROJECT_ID optional +FEISHU_TASK_SECTION_ID optional +--send required for real creation +``` + +Current behavior: + +```text ++task-preview generates local task candidates. ++task-create previews by default and creates tasks only with --send. +Task candidates are derived from workflow recommendations, high-risk issues, +missing-info issues, high-risk PRs, and review-focus items. +Local dedupe uses stable unique_key generation. +``` + +Local test-enterprise validation on 2026-06-26: + +```text ++task-preview generated 7 task candidates from the Gitlink/gitlink-cli report. ++task-create --send created 7 Feishu tasks. +The task result table now shows per-task create status and redacted task IDs. +``` + +Known blockers: + +```text +Feishu-side dedupe/search is not implemented. +Task project and section IDs are collected and redacted in output, but the +current OpenAPI request body only sends summary and description. Project/section +placement must be wired only after the official request fields and tenant +behavior are confirmed in the test enterprise. +``` + +Next hardening: + +```text +Confirm official Task project/section placement fields. +Add Feishu-side dedupe or external unique_key linking when a stable API path exists. +Add scope diagnostics through +app-check. +``` + +## i18n Validation + +Current Feishu commands can consume a Chinese workflow report and render +localized Feishu output: + +```text +workflow +repo-report --lang zh-CN +feishu +notify --lang zh-CN +feishu +owner-digest --lang zh-CN +feishu +contributor-digest --lang zh-CN +feishu +doc-export --lang zh-CN +feishu +task-preview --lang zh-CN +feishu +task-create --lang zh-CN +``` + +Validated output surfaces: + +```text +card field labels +owner/contributor digest headings +common workflow recommendations +DocX block headings +task candidate titles and descriptions +table/markdown preview labels +``` + +Repository-wide i18n check still reports that `internal/i18n/locales/en-US.json` +needs formatting. That is outside the Feishu module and was left untouched to +avoid unrelated locale-file churn. + +## Layer 3: Future Callback-Based GitLink Action Gateway + +No callback server or GitLink write action is implemented in this branch. + +Planned Feishu API families: + +```text +Card callback verification +Event subscription / long connection or callback endpoint +IM message update or follow-up message +User identity lookup: open_id / union_id / email +Chat membership or chat metadata where needed +``` + +Planned GitLink command families: + +```text +Read: +workflow +repo-report +issue +list / +view +pr +list / +view / +files / +diff / +reviews +ci +builds / +logs +pipeline +list / +view / +runs / +results + +Low-risk future writes: +issue +comment +pr +comment +pr +review + +High-risk future writes disabled by default: +pr +merge +issue +close +member +add / +remove / +role +webhook +create / +update / +delete +branch or release deletion +``` + +Required gateway controls: + +```text +Verify Feishu callback signature. +Resolve repo binding. +Map Feishu identity to GitLink identity. +Check GitLink permission. +Generate GitLink dry-run preview. +Require explicit confirmation. +Write audit logs. +Disable high-risk actions by default. +Never execute GitLink writes from a custom bot webhook. +``` + +## GitLink Data Source Inventory + +Current Feishu commands primarily consume: + +```text +workflow +repo-report --format json +``` + +Current source properties: + +```text +Read-only. +Works with local JSON fixture or remote GitLink report generation. +Does not require the Feishu module to know a GitLink token. +Provides summary-level issue, PR, contributor, recommendation, and health fields. +``` + +Required future source expansion: + +```text +PR row source: +pr +list +pr +view +pr +files +pr +diff +pr +versions +pr +reviews + +Issue row source: +issue +list +issue +view +issue metadata commands + +CI / pipeline row source: +ci +builds +ci +logs +pipeline +runs +pipeline +results + +Milestone / release source: +milestone and release commands where available + +Audit source: +future action gateway audit log +``` + +Reason: + +```text +Summary buckets are enough for cards and weekly reports. +Kanban, Gantt, Calendar, Gallery, dashboard, and personal task panels require +row-level GitLink records. +``` + +## Manual Setup Required From User + +Stable webhook validation: + +```text +1. Add a custom bot to the target Feishu test group. +2. Copy the webhook URL into FEISHU_WEBHOOK_URL. +3. Copy the signing secret into FEISHU_WEBHOOK_SECRET if signing is enabled. +4. Run +bot-test or +notify with --send. +``` + +DocX / Wiki validation: + +```text +1. Confirm FEISHU_APP_ID and FEISHU_APP_SECRET for the self-built app. +2. Approve required DocX / Drive scopes in Feishu Open Platform. +3. Grant the app edit access to the target Wiki / DocX page, or provide a + folder token where the app can create documents. +4. Run +doc-export first without --send, then with --send. +``` + +Bitable validation: + +```text +1. Create or choose a Base manually. +2. Create reports, issues, prs, contributors, and tasks tables manually. +3. Add a unique_key field to every table. +4. Copy FEISHU_BASE_APP_TOKEN and each table ID into local env. +5. Grant the self-built app Base/Bitable access. +6. Run +bitable-sync without --send first, then with --send. +``` + +For a quick validation, multiple table env vars can point to the same test +table if that table has every required field. For a real project cockpit, prefer +separate tables or a row-level model that supports Kanban, Gantt, Calendar, +Gallery, Form, and Dashboard views without mixing incompatible record groups. + +Task validation: + +```text +1. Confirm Task API scopes for the self-built app. +2. Decide whether tasks should be created as plain tasks first. +3. Do not rely on project/section placement until request fields are verified. +4. Run +task-preview first, then +task-create --send. +``` + +GitLink real data validation: + +```text +1. Set GITLINK_OWNER and GITLINK_REPO. +2. Set GITLINK_TEST_PR_IDS for smoke report reference. +3. Ensure gitlink-cli can generate workflow +repo-report JSON. +4. Do not commit GitLink tokens or account credentials. +``` + +## Acceptance Checklist For API Collection + +```text +[x] Custom bot webhook API identified. +[x] Custom bot signing behavior mapped to current code. +[x] tenant_access_token API identified and implemented. +[x] Wiki node resolution API identified and implemented. +[x] DocX create and block append APIs identified and implemented. +[x] Bitable record search/create/update APIs identified and implemented. +[x] Task create API identified and implemented at minimal summary/description level. +[x] IM app bot send API identified as planned, not implemented. +[x] Card callback/event subscription identified as future, not implemented. +[x] User identity APIs identified as future, not implemented. +[x] GitLink read data source boundary documented. +[x] GitLink write action boundary documented as not implemented. +[x] Required local env variables documented. +[x] Resource-level permission requirements documented. +[x] Remaining user/manual setup steps documented. +``` diff --git a/docs/PR_VISUAL_GUIDE.md b/docs/PR_VISUAL_GUIDE.md index 1a210f2..e522602 100644 --- a/docs/PR_VISUAL_GUIDE.md +++ b/docs/PR_VISUAL_GUIDE.md @@ -3,6 +3,9 @@ Date: 2026-06-26 This file lists the manual screenshots to capture after local and real smoke testing. +The 2026-06-26 smoke run successfully delivered Feishu cards, appended DocX +content, synced Bitable records, and created Feishu tasks in the test +enterprise. Screenshots still need to be captured manually from the UI. Do not fabricate screenshots. If a capability is not available in the test enterprise, keep the placeholder and record the failure in `reports/FEISHU_SMOKE_20260626.md`. @@ -19,9 +22,9 @@ Use the helper to check current screenshot status: | Owner digest card | `docs/images/feishu-owner-digest.png` | Capture after `+owner-digest --send` | | Contributor digest card | `docs/images/feishu-contributor-digest.png` | Capture after `+contributor-digest --send` | | Bitable records preview | `docs/images/feishu-bitable-preview.png` | Capture terminal output or JSON preview | -| Bitable Base after sync | `docs/images/feishu-bitable-sync.png` | Capture only if real sync succeeds | -| DocX / Wiki report | `docs/images/feishu-docx-wiki.png` | Capture only if real document write succeeds | -| Feishu task list | `docs/images/feishu-task-create.png` | Capture only if real task creation succeeds | +| Bitable Base after sync | `docs/images/feishu-bitable-sync.png` | Real sync succeeded in the test Base; capture the updated table or target view | +| DocX / Wiki report | `docs/images/feishu-docx-wiki.png` | Real DocX append succeeded; capture the appended report blocks | +| Feishu task list | `docs/images/feishu-task-create.png` | Real task creation succeeded; capture the created task list and redact IDs if visible | | Terminal smoke test summary | `docs/images/feishu-smoke-terminal.png` | Redact IDs and tokens | | Redacted env check | `docs/images/feishu-env-redacted.png` | Show presence/absence only | @@ -31,6 +34,7 @@ Suggested capture commands: gitlink-cli feishu +owner-digest --from-workflow-json report.json --send --format table gitlink-cli feishu +contributor-digest --from-workflow-json report.json --send --format table gitlink-cli feishu +bitable-records --from-workflow-json report.json --format table +gitlink-cli feishu +notify --from-workflow-json report.zh-CN.json --lang zh-CN --send --format table ``` Manual redaction checklist: diff --git a/reports/FEISHU_API_COLLECTION_CHECKLIST_20260626.md b/reports/FEISHU_API_COLLECTION_CHECKLIST_20260626.md new file mode 100644 index 0000000..75ceafb --- /dev/null +++ b/reports/FEISHU_API_COLLECTION_CHECKLIST_20260626.md @@ -0,0 +1,192 @@ +# Feishu API Collection Checklist + +Date: 2026-06-26 + +Branch: + +```text +feat/feishu-export-clean +``` + +Current commit at collection time: + +```text +73da46c143b37cb2b26e9e624b8c39963ad52d77 +``` + +## Current Local State + +```text +Feishu desktop/web state: test account is logged in. +Local env file: .local/feishu-gitlink.env.ps1 is configured and ignored. +Stable previews: available from .local/report.json and .local/report.zh-CN.json. +Real Feishu sends: passed through custom bot webhook. +Real DocX append: passed through self-built app OpenAPI. +Real Bitable sync: passed after target table fields were created. +Real Task create: passed; project/section placement remains unmapped. +GitLink write operations: not implemented and not tested. +``` + +## API Collection Status + +| Item | Status | Evidence | Next action | +| --- | --- | --- | --- | +| Custom bot webhook | Complete and real-tested | `shortcuts/feishu/client.go`, `sign.go`, `card.go` | Capture screenshots | +| Custom bot signing | Complete and real-tested | `SignCustomBotRequest` unit test plus signed bot smoke | Keep secrets redacted | +| tenant_access_token | Complete and real-tested | `OpenAPIClient.TenantAccessToken` | Add future `+app-check` | +| Wiki node resolution | Complete | `OpenAPIClient.GetWikiNode` | Still depends on target Wiki node permission | +| DocX create | Complete | `OpenAPIClient.CreateDocument` | Requires folder permission when creating new docs | +| DocX block append | Complete and real-tested | `OpenAPIClient.CreateBlocks` | App must have target DocX edit permission | +| Bitable search | Complete and real-tested | `SearchBitableRecord` | Requires `unique_key` field | +| Bitable create | Complete and real-tested | `CreateBitableRecord` | Requires existing table and compatible fields | +| Bitable update | Complete and real-tested | `UpdateBitableRecord` | Never deletes records | +| Task create | Complete at minimal level and real-tested | `CreateTask` sends summary and description | Confirm project/section request fields | +| IM app bot message | Planned | Official API collected | Not needed for stable webhook path | +| Card callbacks | Future | Official capability identified | Requires server, signature validation, identity mapping | +| User identity mapping | Future | Required for personalized contributor routing | Not implemented in this branch | +| GitLink write actions | Explicitly out of scope | Capability boundary docs | Do not implement in this branch | + +## Command Checklist + +| Command | Layer | Current status | Real side effect? | Needs user setup? | +| --- | --- | --- | --- | --- | +| `feishu +bot-test` | Stable | Implemented | Only with `--send` | `FEISHU_WEBHOOK_URL` | +| `feishu +notify` | Stable | Implemented | Only with `--send` | `FEISHU_WEBHOOK_URL` | +| `feishu +weekly-report` | Stable | Implemented | Only with `--send` | `FEISHU_WEBHOOK_URL` | +| `feishu +owner-digest` | Stable | Implemented | Only with `--send` | `FEISHU_WEBHOOK_URL` | +| `feishu +contributor-digest` | Stable | Implemented | Only with `--send` | `FEISHU_WEBHOOK_URL` | +| `feishu +bitable-schema` | Stable dry-run | Implemented | No | No | +| `feishu +bitable-records` | Stable dry-run | Implemented | No | No | +| `feishu +task-preview` | Stable dry-run | Implemented | No | No | +| `feishu +doc-export` | Experimental | Implemented and real-tested | Only with `--send` | App scopes and document/folder permission | +| `feishu +bitable-sync` | Experimental | Implemented and real-tested | Only with `--send` | Base app token, table IDs, fields, scopes | +| `feishu +task-create` | Experimental | Implemented and real-tested at minimal level | Only with `--send` | Task scopes; project/section placement pending | + +## What Is Complete + +```text +1. Feishu API families are mapped to current commands. +2. Current code endpoints are inventoried. +3. Stable custom bot boundary is clear. +4. Experimental Open Platform boundary is clear. +5. GitLink write action boundary is clear. +6. User-required environment variables are documented. +7. Resource-level permission requirements are documented. +8. Task project/section limitation is explicitly called out. +``` + +## What Still Needs User Action + +These remain manual or owner-side tasks and should not be committed to the +repository. + +```text +1. Capture Feishu UI screenshots for the PR visual guide. +2. Decide whether the test Base should use one table with views or separate + reports/issues/prs/contributors/tasks tables. +3. If separate tables are desired, create them and copy each table ID into the + local env file. +4. Decide whether `+bitable-sync` should stay experimental or be narrowed to + dry-run-only for upstream review. +5. Confirm Feishu Task project/section request fields before placing tasks in + a specific project or section. +6. Keep all real app credentials, webhook URLs, table IDs, and tokens in local + env only. +``` + +## Commands To Run After User Setup + +Preview first: + +```powershell +.\scripts\feishu-gitlink-env-check.ps1 -Layer all + +go run . feishu +notify --from-workflow-json .local\report.json --format table +go run . feishu +owner-digest --from-workflow-json .local\report.json --format table +go run . feishu +contributor-digest --from-workflow-json .local\report.json --format table +go run . feishu +bitable-records --from-workflow-json .local\report.json --format json +go run . feishu +bitable-sync --from-workflow-json .local\report.json --format table +go run . feishu +doc-export --from-workflow-json .local\report.json --format table +go run . feishu +task-preview --from-workflow-json .local\report.json --format markdown +``` + +Real sends/writes only after preview is correct: + +```powershell +go run . feishu +notify --from-workflow-json .local\report.json --send --format table +go run . feishu +weekly-report --from-workflow-json .local\report.json --send --format table +go run . feishu +owner-digest --from-workflow-json .local\report.json --send --format table +go run . feishu +contributor-digest --from-workflow-json .local\report.json --send --format table +go run . feishu +doc-export --from-workflow-json .local\report.json --send --format table +go run . feishu +bitable-sync --from-workflow-json .local\report.json --send --format table +go run . feishu +task-create --from-workflow-json .local\report.json --send --format table +``` + +Test suite: + +```powershell +gofmt -w shortcuts\feishu +go test ./shortcuts/feishu +go test ./shortcuts/workflow +go test ./shortcuts +go test ./... +``` + +## Current Blockers + +```text +Task project/section placement needs official request-field confirmation. +Current Base output is summary-oriented and not yet row-level project cockpit data. +Screenshot evidence still needs manual capture. +``` + +## Verification Run + +Executed on 2026-06-26 after the API inventory update: + +| Check | Result | Notes | +| --- | --- | --- | +| Computer-use Feishu desktop read-only check | Pass | Feishu test account visible | +| `go run . feishu --help` | Pass | Expected stable and experimental commands are registered | +| Env check | Pass | Required stable and Open Platform variables present; task project/section optional | +| `+notify` preview | Pass | Local preview mode | +| `+owner-digest` preview | Pass | Repository `Gitlink/gitlink-cli`, risk `high`, score `49` | +| `+contributor-digest` preview | Pass | Role-oriented digest, not personalized routing | +| `+notify --send` | Pass | Custom bot delivered English/default and Chinese cards | +| `+weekly-report --send` | Pass | Custom bot delivered weekly report | +| `+owner-digest --send` | Pass | Custom bot delivered English/default and Chinese owner digest | +| `+contributor-digest --send` | Pass | Custom bot delivered English/default and Chinese contributor digest | +| `+bitable-sync` preview | Pass | 1 report, 5 issue, 2 PR, 1 contributor, 7 task records | +| `+bitable-sync --send` | Pass | Search/create/update real-tested after field creation | +| `+doc-export` preview | Pass | 9 DocX-ready blocks | +| `+doc-export --send` | Pass | Appended English/default and Chinese DocX blocks | +| `+task-preview` preview | Pass | 7 task candidates | +| `+task-create --send` | Pass | 7 tasks created; reruns may duplicate tasks | +| Feishu command i18n | Pass | zh-CN cards, digest, DocX blocks, and task titles previewed/sent | +| `go run ./internal/i18n/cmd/check` | Expected fail | Existing `en-US.json` formatting issue outside Feishu module | +| `go test ./shortcuts/feishu` | Pass | Includes task preview count regression test | +| `go test ./shortcuts/workflow` | Pass | Workflow report source remains valid | +| `go test ./shortcuts` | Pass | Shortcut package regression passed | +| `go test ./...` | Pass | Full repository test suite passed | +| Raw secret scan | Pass | No raw secret values found in tracked/unignored candidate files | +| Screenshot checklist | Expected fail | Real send/write screenshots still need manual capture | + +## Do Not Commit + +```text +FEISHU_WEBHOOK_URL +FEISHU_WEBHOOK_SECRET +FEISHU_APP_ID +FEISHU_APP_SECRET +tenant_access_token +user_access_token +FEISHU_BASE_APP_TOKEN +table IDs +Wiki node token +folder token +chat_id +open_id +union_id +GITLINK_TOKEN +personal account credentials +``` diff --git a/reports/FEISHU_PERMISSION_MATRIX.md b/reports/FEISHU_PERMISSION_MATRIX.md index 2671e4b..c781b18 100644 --- a/reports/FEISHU_PERMISSION_MATRIX.md +++ b/reports/FEISHU_PERMISSION_MATRIX.md @@ -6,15 +6,15 @@ GitLink write permission is `No` for every implemented command in this branch. | Capability | Command | Layer | Needs webhook? | Needs app_id/app_secret? | Needs DocX/Wiki scope? | Needs Base scope? | Needs Task scope? | Needs GitLink token? | Needs GitLink write permission? | Tested locally? | Test result | Known limitation | | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | -| Custom bot test | `feishu +bot-test` | Stable webhook export | Yes for `--send` | No | No | No | No | No | No | Yes | unit/mock; real if webhook env exists | Custom bot only posts to configured chat | -| Workflow card | `feishu +notify` | Stable webhook export | Yes for `--send` | No | No | No | No | No | No | Yes | preview passed | Consumes workflow JSON; no direct Feishu identity routing | -| Weekly report | `feishu +weekly-report` | Stable webhook export | Yes for `--send` | No | No | No | No | No | No | Yes | preview passed | Card is summary-level | -| Owner digest | `feishu +owner-digest` | Stable webhook export | Yes for `--send` | No | No | No | No | No | No | Yes | unit and preview passed | Role-oriented, not personalized | -| Contributor digest | `feishu +contributor-digest` | Stable webhook export | Yes for `--send` | No | No | No | No | No | No | Yes | unit and preview passed | Role-oriented, not open_id routed | +| Custom bot test | `feishu +bot-test` | Stable webhook export | Yes for `--send` | No | No | No | No | No | No | Yes | unit/mock and real send passed | Custom bot only posts to configured chat | +| Workflow card | `feishu +notify` | Stable webhook export | Yes for `--send` | No | No | No | No | No | No | Yes | preview and real send passed, including zh-CN | Consumes workflow JSON; no direct Feishu identity routing | +| Weekly report | `feishu +weekly-report` | Stable webhook export | Yes for `--send` | No | No | No | No | No | No | Yes | preview and real send passed | Card is summary-level | +| Owner digest | `feishu +owner-digest` | Stable webhook export | Yes for `--send` | No | No | No | No | No | No | Yes | unit, preview, and real send passed, including zh-CN | Role-oriented, not personalized | +| Contributor digest | `feishu +contributor-digest` | Stable webhook export | Yes for `--send` | No | No | No | No | No | No | Yes | unit, preview, and real send passed, including zh-CN | Role-oriented, not open_id routed | | Bitable schema | `feishu +bitable-schema` | Stable dry-run | No | No | No | No | No | No | No | Yes | preview passed | Does not create tables or views | | Bitable records | `feishu +bitable-records` | Stable dry-run | No | No | No | No | No | No | No | Yes | preview passed | Summary records, not one row per raw issue/PR | -| Task preview | `feishu +task-preview` | Stable dry-run | No | No | No | No | No | No | No | Yes | preview passed | Local candidates only | -| DocX / Wiki export | `feishu +doc-export` | Experimental Open Platform | No | Yes for `--send` | Yes | No | No | No | No | Mocked; real depends on env | mock passed | App must have scopes and document/folder permission | -| Bitable sync | `feishu +bitable-sync` | Experimental Open Platform | No | Yes for `--send` | No | Yes | No | No | No | Mocked; real depends on env | mock passed | Requires existing tables and `unique_key` field | -| Task create | `feishu +task-create` | Experimental Open Platform | No | Yes for `--send` | No | No | Yes | No | No | Mocked; real depends on env | mock passed | Dedupe is local unique_key only | +| Task preview | `feishu +task-preview` | Stable dry-run | No | No | No | No | No | No | No | Yes | preview passed, including zh-CN | Local candidates only | +| DocX / Wiki export | `feishu +doc-export` | Experimental Open Platform | No | Yes for `--send` | Yes | No | No | No | No | Yes | mock, preview, and real DocX append passed, including zh-CN | App must have scopes and document/folder permission | +| Bitable sync | `feishu +bitable-sync` | Experimental Open Platform | No | Yes for `--send` | No | Yes | No | No | No | Yes | mock, preview, and real search/create/update passed | Requires existing tables and compatible fields; one-table test used multiple record groups | +| Task create | `feishu +task-create` | Experimental Open Platform | No | Yes for `--send` | No | No | Yes | No | No | Yes | mock, preview, and real create passed | Dedupe is local unique_key only; project/section IDs are collected but not mapped into the request body yet | | GitLink action gateway | not implemented | Future planning | No | Planned | No | No | No | Planned | Yes | No | not implemented | Requires official authorization model | diff --git a/reports/FEISHU_SMOKE_20260626.md b/reports/FEISHU_SMOKE_20260626.md index 2acba26..f396e9d 100644 --- a/reports/FEISHU_SMOKE_20260626.md +++ b/reports/FEISHU_SMOKE_20260626.md @@ -1,6 +1,6 @@ -# Feishu Smoke Report +# Feishu Smoke Report -Date: 2026-06-26 16:29:18 +08:00 +Date: 2026-06-26 20:58:49 +08:00 ## Branch @@ -11,67 +11,178 @@ feat/feishu-export-clean ## Commit ```text -9255518304e1a6b0fba9f9e5eee9bdf4f62d8e04 +73da46c143b37cb2b26e9e624b8c39963ad52d77 ``` +The worktree was dirty during this smoke run because the Feishu implementation +and documentation were still being updated. + ## Mode ```text -preview +real Feishu test enterprise plus local previews ``` +## Test Environment + +```text +Feishu test enterprise: used +Custom bot in test group: used +Self-built app with broad test permissions: used +Feishu DocX target: used +Feishu Base target: used +Feishu Task API: used +GitLink real repository data: Gitlink/gitlink-cli +Reference PR IDs for smoke notes: 95, 29, 75 +GitLink write operations: not used +``` + +All Feishu resource IDs, tokens, webhook URLs, app credentials, table IDs, and +document IDs were kept in `.local/feishu-gitlink.env.ps1` and are not committed. + ## Redacted Environment Presence -| Variable | Present? | -| --- | --- | -| `FEISHU_WEBHOOK_URL` | missing | -| `FEISHU_WEBHOOK_SECRET` | missing | -| `FEISHU_APP_ID` | missing | -| `FEISHU_APP_SECRET` | missing | -| `FEISHU_WIKI_URL` | missing | -| `FEISHU_WIKI_NODE_TOKEN` | missing | -| `FEISHU_FOLDER_TOKEN` | missing | -| `FEISHU_BASE_APP_TOKEN` | missing | -| `FEISHU_REPORT_TABLE_ID` | missing | -| `FEISHU_ISSUE_TABLE_ID` | missing | -| `FEISHU_PR_TABLE_ID` | missing | -| `FEISHU_CONTRIBUTOR_TABLE_ID` | missing | -| `FEISHU_TASK_TABLE_ID` | missing | -| `FEISHU_TASK_PROJECT_ID` | missing | -| `FEISHU_TASK_SECTION_ID` | missing | -| `GITLINK_OWNER` | missing | -| `GITLINK_REPO` | missing | -| `GITLINK_TEST_PR_IDS` | missing | -| `GITLINK_TOKEN` | missing | +| Variable | Present? | Notes | +| --- | --- | --- | +| `FEISHU_WEBHOOK_URL` | present | redacted in CLI output | +| `FEISHU_WEBHOOK_SECRET` | present | redacted in CLI output | +| `FEISHU_APP_ID` | present | redacted where printed | +| `FEISHU_APP_SECRET` | present | never printed | +| `FEISHU_FOLDER_TOKEN` | present | redacted | +| `FEISHU_DOCUMENT_ID` | present | redacted | +| `FEISHU_BASE_APP_TOKEN` | present | redacted | +| `FEISHU_REPORT_TABLE_ID` | present | same test table as other table envs | +| `FEISHU_ISSUE_TABLE_ID` | present | same test table as other table envs | +| `FEISHU_PR_TABLE_ID` | present | same test table as other table envs | +| `FEISHU_CONTRIBUTOR_TABLE_ID` | present | same test table as other table envs | +| `FEISHU_TASK_TABLE_ID` | present | same test table as other table envs | +| `FEISHU_TASK_PROJECT_ID` | missing | optional; current request body does not place tasks into project/section | +| `FEISHU_TASK_SECTION_ID` | missing | optional; current request body does not place tasks into project/section | +| `GITLINK_OWNER` | present | `Gitlink` | +| `GITLINK_REPO` | present | `gitlink-cli` | +| `GITLINK_TEST_PR_IDS` | present | `95,29,75` | +| `GITLINK_TOKEN` | missing | not required for the read-only workflow report in this run | -## Results +## GitLink Report Source + +Command: + +```powershell +go run . workflow +repo-report --owner $env:GITLINK_OWNER --repo $env:GITLINK_REPO --format json > .local\report.json +go run . workflow +repo-report --owner $env:GITLINK_OWNER --repo $env:GITLINK_REPO --lang zh-CN --format json > .local\report.zh-CN.json +``` + +Result: + +| Item | Value | +| --- | --- | +| Repository | `Gitlink/gitlink-cli` | +| Report score | `49` | +| Risk level | `high` | +| Health score | `58` | +| Issues | `19` | +| Pull requests | `10` | +| Source | `remote-read-only-fetch` | + +The workflow command does not currently filter the report by explicit PR IDs, so +`GITLINK_TEST_PR_IDS` is recorded as smoke context rather than a hard filter. + +## Real Feishu Results | Command | Result | Details | | --- | --- | --- | -| feishu help | pass | exit=0 | -| feishu +owner-digest help | pass | exit=0 | -| feishu +contributor-digest help | pass | exit=0 | -| feishu +bitable-sync help | pass | exit=0 | -| feishu +task-preview help | pass | exit=0 | -| feishu +task-create help | pass | exit=0 | -| workflow +repo-report | pass | report=.local/report.json; owner=Gitlink; repo=gitlink-cli | -| notify preview | pass | exit=0 | -| weekly report preview | pass | exit=0 | -| owner digest preview | pass | exit=0 | -| contributor digest preview | pass | exit=0 | -| bitable records preview | pass | exit=0 | -| task preview | pass | exit=0 | +| `feishu +bot-test --send` | pass | custom bot returned Feishu code `0` | +| `feishu +notify --send` | pass | English/default workflow card delivered | +| `feishu +weekly-report --send` | pass | weekly report card delivered | +| `feishu +owner-digest --send` | pass | owner digest card delivered | +| `feishu +contributor-digest --send` | pass | contributor digest card delivered | +| `feishu +notify --lang zh-CN --send` | pass | Chinese workflow card delivered | +| `feishu +owner-digest --lang zh-CN --send` | pass | Chinese owner digest delivered | +| `feishu +contributor-digest --lang zh-CN --send` | pass | Chinese contributor digest delivered | +| `feishu +doc-export --send` | pass | appended 9 DocX blocks to the configured document | +| `feishu +doc-export --lang zh-CN --send` | pass | appended 9 localized DocX blocks | +| `feishu +bitable-sync --tables reports --send` | pass after table fields were added | created the report record | +| `feishu +bitable-sync --tables reports,issues,prs,contributors,tasks --send` | pass | updated 1 report, created 5 issue buckets, 2 PR buckets, 1 contributor bucket, 7 task buckets | +| `feishu +bitable-sync --lang zh-CN --send` | pass | updated existing records from the Chinese workflow JSON | +| `feishu +task-preview --lang zh-CN` | pass | generated 7 Chinese task candidates | +| `feishu +task-create --lang zh-CN --send` | pass | created 7 Feishu tasks | -## Notes +## Bitable Setup Observation -- No .local/feishu-gitlink.env.ps1 file found. Preview smoke can run with public fallback data; real sends are skipped. -- GITLINK_OWNER/GITLINK_REPO were missing. Preview smoke used public Gitlink/gitlink-cli as a fallback. +The provided Feishu Base URLs pointed to one Base and one table with multiple +views. The test enterprise initially had only the default fields. A direct +OpenAPI inspection found one table and the default fields only, so the test +table was expanded with the fields expected by the CLI records: -## Terminal Log +```text +unique_key, repository, health_score, risk_level, report_score, +issue_total, issue_high_risk, issue_missing_info, pr_total, pr_high_risk, +review_focus_count, generated_at, source, doc_url, issue_group, priority, +count, risk_reason, recommended_action, gitlink_url, pr_group, review_focus, +contributor, role, open_items, risk_items, task_title, task_type, source_type, +source_key, recommended_owner, status, due_hint +``` -Local redacted terminal log: `reports/feishu-real-smoke-terminal.log` +This confirms that `+bitable-sync` can search, create, and update records when +the target table already has compatible fields. It does not yet create Base +tables or views itself. -This log file is ignored and should not be committed after real runs. +## i18n Result + +Feishu command-level Chinese output is usable: + +```text +workflow +repo-report --lang zh-CN +feishu +notify --lang zh-CN +feishu +owner-digest --lang zh-CN +feishu +contributor-digest --lang zh-CN +feishu +doc-export --lang zh-CN +feishu +task-preview --lang zh-CN +feishu +task-create --lang zh-CN +``` + +The Feishu module localizes stable card labels, digest headings, common +recommendations, DocX block headings, and task candidate titles. For best +results, generate the source workflow report with `--lang zh-CN` and pass +`--lang zh-CN` again to the Feishu command. + +Repository-wide i18n formatting check: + +```text +go run ./internal/i18n/cmd/check +``` + +Result: + +```text +fail: internal/i18n/locales/en-US.json is not formatted +``` + +That appears to be an existing locale formatting issue outside the Feishu +module. It was not fixed in this smoke run to avoid unrelated locale churn. + +## Tests + +| Check | Result | +| --- | --- | +| `go test ./shortcuts/feishu` | pass | +| `go test ./shortcuts/workflow` | pass | +| `go test ./shortcuts` | pass | +| `go test ./...` | pass | +| Raw secret scan over tracked/unignored candidate files | pass | + +## Known Limitations + +```text +1. Bitable sync requires existing Base/table/fields; CLI does not create tables or views. +2. The current smoke used one test table for all record groups because the provided links were one table with multiple views. +3. Current Bitable records are summary buckets, not row-level PR/Issue/CI records. +4. Feishu task creation does not yet map project/section placement into the request body. +5. Feishu-side task dedupe/search is not implemented; avoid repeated real task-create runs unless duplicates are acceptable. +6. No Feishu callback server is implemented. +7. No GitLink write operation is implemented. +8. Screenshots still need to be captured manually from the Feishu UI. +``` ## Screenshot Checklist @@ -81,4 +192,19 @@ Run: .\scripts\feishu-gitlink-screenshot-check.ps1 ``` -Do not fabricate screenshots. Capture missing images manually after real Feishu runs. +Manual captures still needed: + +```text +docs/images/feishu-bot-card.png +docs/images/feishu-weekly-report.png +docs/images/feishu-owner-digest.png +docs/images/feishu-contributor-digest.png +docs/images/feishu-bitable-preview.png +docs/images/feishu-bitable-sync.png +docs/images/feishu-docx-wiki.png +docs/images/feishu-task-create.png +docs/images/feishu-smoke-terminal.png +docs/images/feishu-env-redacted.png +``` + +Do not fabricate screenshots. Redact IDs and tokens before committing any image. diff --git a/reports/FEISHU_USER_COLLECTION_LIST_20260626.md b/reports/FEISHU_USER_COLLECTION_LIST_20260626.md new file mode 100644 index 0000000..0227b90 --- /dev/null +++ b/reports/FEISHU_USER_COLLECTION_LIST_20260626.md @@ -0,0 +1,175 @@ +# 飞书 / GitLink 本地验证信息收集清单 + +Date: 2026-06-26 + +用途:这份清单只说明需要从飞书和 GitLink 页面收集哪些值。真实值不要写进本文件,也不要提交到仓库。真实值只放到本地忽略文件: + +```text +.local/feishu-gitlink.env.ps1 +``` + +## 当前状态 + +```text +自定义机器人 webhook:已配置并真实发送通过。 +自建应用 app_id/app_secret:已配置并获取 tenant_access_token 通过。 +DocX 目标:已配置并真实追加报告通过。 +多维表格 Base:已配置;当前测试链接是同一个 Base 的同一张表的多个视图。 +多维表格字段:已通过 OpenAPI 为测试表补齐。 +Bitable search/create/update:已真实通过。 +飞书任务创建:已真实通过;项目/分组归属尚未接入请求体。 +GitLink 仓库:已使用 Gitlink/gitlink-cli 生成真实 workflow report。 +i18n:feishu 命令 zh-CN 输出可用;仓库全局 i18n check 仍有既有 en-US.json 格式化问题。 +截图:仍需从飞书 UI 手工截取。 +``` + +## 1. 稳定层:飞书自定义机器人 + +这些值用于真实发送飞书群卡片。 + +| 需要收集 | 填入变量 | 是否敏感 | 获取位置 | 当前用途 | +| --- | --- | --- | --- | --- | +| 自定义机器人 Webhook URL | `FEISHU_WEBHOOK_URL` | 是 | 飞书群聊 -> 群设置 -> 机器人 -> 自定义机器人 | `+bot-test`, `+notify`, `+weekly-report`, `+owner-digest`, `+contributor-digest --send` | +| 自定义机器人签名密钥 | `FEISHU_WEBHOOK_SECRET` | 是 | 自定义机器人安全设置,若开启签名 | 同上 | + +最小可验证: + +```text +只要有 FEISHU_WEBHOOK_URL,就可以先测试稳定消息卡片。 +如果机器人开启了签名,还必须填 FEISHU_WEBHOOK_SECRET。 +``` + +## 2. 飞书开放平台自建应用 + +这些值用于 DocX、Wiki、多维表格、任务等实验性 OpenAPI 写入。 + +| 需要收集 | 填入变量 | 是否敏感 | 获取位置 | 当前用途 | +| --- | --- | --- | --- | --- | +| App ID | `FEISHU_APP_ID` | 是 | 飞书开放平台 -> 自建应用 -> 凭证与基础信息 | `+doc-export`, `+bitable-sync`, `+task-create --send` | +| App Secret | `FEISHU_APP_SECRET` | 是 | 同上 | 获取 `tenant_access_token` | + +需要确认: + +```text +1. 应用已经创建。 +2. 应用在测试企业内可用。 +3. 需要的 API 权限已经申请或开通。 +4. 目标文档、知识库、多维表格或任务空间已经给应用必要权限。 +``` + +## 3. DocX / Wiki 验证目标 + +这些值用于把 GitLink workflow report 写入飞书云文档或知识库。 + +| 需要收集 | 填入变量 | 是否敏感 | 获取位置 | 当前用途 | +| --- | --- | --- | --- | --- | +| Wiki 页面 URL | `FEISHU_WIKI_URL` | 可能敏感 | 目标飞书知识库页面地址栏 | `+doc-export --wiki-url ... --send` | +| Wiki node token | `FEISHU_WIKI_NODE_TOKEN` | 是 | 可从 Wiki URL 解析,或 OpenAPI 返回 | `+doc-export` | +| 文件夹 token | `FEISHU_FOLDER_TOKEN` | 是 | 飞书云空间文件夹 URL | 创建新 DocX | +| 已有 DocX document ID | `FEISHU_DOCUMENT_ID` | 是 | DocX URL 或 OpenAPI 返回 | 追加已有 DocX | + +三选一即可开始: + +```text +方案 A:提供 FEISHU_WIKI_URL,让命令解析 Wiki node。 +方案 B:提供 FEISHU_FOLDER_TOKEN,让命令新建 DocX。 +方案 C:提供 FEISHU_DOCUMENT_ID,追加已有 DocX。 +``` + +必须人工处理: + +```text +gitlink-cli 不会替你修改飞书文档权限。 +你需要在飞书里给自建应用目标文档、知识库或文件夹的编辑权限。 +``` + +## 4. 多维表格 Base / Bitable + +这些值用于实验性真实同步记录。 + +| 需要收集 | 填入变量 | 是否敏感 | 获取位置 | 当前用途 | +| --- | --- | --- | --- | --- | +| Base app token | `FEISHU_BASE_APP_TOKEN` | 是 | 多维表格 URL 或开发者工具 API | `+bitable-sync --send` | +| reports 表 ID | `FEISHU_REPORT_TABLE_ID` | 是 | 多维表格表设置/API | 报告汇总行 | +| issues 表 ID | `FEISHU_ISSUE_TABLE_ID` | 是 | 同上 | Issue 汇总行 | +| prs 表 ID | `FEISHU_PR_TABLE_ID` | 是 | 同上 | PR 汇总行 | +| contributors 表 ID | `FEISHU_CONTRIBUTOR_TABLE_ID` | 是 | 同上 | 贡献者汇总行,可选 | +| tasks 表 ID | `FEISHU_TASK_TABLE_ID` | 是 | 同上 | 任务候选行,可选 | + +当前测试说明: + +```text +你提供的多维表格链接当前是同一个 Base 的同一张表,只是不同视图。 +为了验证 OpenAPI 写入,我把 reports/issues/prs/contributors/tasks 都指向了同一张测试表,并补齐了需要字段。 +这适合验证 search/create/update,但不是最终项目驾驶舱模型。 +``` + +正式模型建议: + +```text +1. 要么拆成 reports / issues / prs / contributors / tasks 多张表。 +2. 要么改成更强的行级统一模型,支持看板、甘特图、日历、画册、表单和仪表盘。 +3. 当前 CLI 不自动创建 Base、表、字段或视图。 +4. Kanban / Gantt / Calendar / Gallery / Dashboard 视图先建议人工配置。 +``` + +## 5. 飞书任务 + +这些值用于实验性创建飞书任务。 + +| 需要收集 | 填入变量 | 是否敏感 | 获取位置 | 当前用途 | +| --- | --- | --- | --- | --- | +| 任务项目 ID | `FEISHU_TASK_PROJECT_ID` | 是 | 飞书任务项目设置/API | 当前仅收集和脱敏输出 | +| 任务分组/section ID | `FEISHU_TASK_SECTION_ID` | 是 | 飞书任务项目设置/API | 当前仅收集和脱敏输出 | + +当前限制: + +```text ++task-create 真实请求目前只发送任务 summary 和 description。 +project / section 设置字段还没有接入请求体。 +已验证普通任务创建;后续再确认项目/分组字段。 +``` + +## 6. GitLink 真实仓库数据 + +这些值用于生成真实 workflow report。 + +| 需要收集 | 填入变量 | 是否敏感 | 获取位置 | 当前用途 | +| --- | --- | --- | --- | --- | +| 仓库 owner | `GITLINK_OWNER` | 否 | GitLink 仓库 URL | `workflow +repo-report` | +| 仓库名 | `GITLINK_REPO` | 否 | GitLink 仓库 URL | `workflow +repo-report` | +| 测试 PR IDs | `GITLINK_TEST_PR_IDS` | 否 | 之前 3 个 PR URL/编号 | 烟测报告记录 | +| GitLink Token | `GITLINK_TOKEN` | 是 | GitLink 账号设置/API token | 若本地未登录且需要远程读取 | + +示例,不要照抄: + +```powershell +$env:GITLINK_OWNER="OWNER" +$env:GITLINK_REPO="REPO" +$env:GITLINK_TEST_PR_IDS="1,2,3" +$env:GITLINK_TOKEN="REDACTED" +``` + +## 7. 仍需人工完成 + +```text +1. 从飞书群里截取 bot card、weekly report、owner digest、contributor digest。 +2. 从飞书多维表格里截取同步后的记录或视图。 +3. 从飞书 DocX 里截取追加后的报告内容。 +4. 从飞书任务里截取创建后的任务列表。 +5. 截图前确认没有暴露 app secret、webhook、token、table id、open_id 或 union_id。 +``` + +截图目标路径见: + +```text +docs/PR_VISUAL_GUIDE.md +``` + +## 8. 安全提醒 + +```text +不要把 app secret、webhook、token、table id、wiki token、folder token 发到公开聊天或提交到仓库。 +真实值只放在 .local/feishu-gitlink.env.ps1。 +如果需要继续真实验证,优先复用本地 env 文件,不要把值写进 docs、reports、README。 +``` diff --git a/shortcuts/feishu/bitable_sync.go b/shortcuts/feishu/bitable_sync.go index 19bf2f5..cb6cbec 100644 --- a/shortcuts/feishu/bitable_sync.go +++ b/shortcuts/feishu/bitable_sync.go @@ -119,10 +119,11 @@ func syncBitableOrPreview(ctx *common.RuntimeContext, opts BitableSyncOptions, r } for _, record := range records.Tables[tableResult.Table] { result := BitableSyncRecordResult{UniqueKey: record.UniqueKey} + fields := normalizeBitableWriteFields(record.Fields) search, err := client.SearchBitableRecord(context.Background(), token.Value, opts.BaseAppToken, tableID, record.UniqueKey) if err != nil { output.Warnings = append(output.Warnings, diagnoseOpenAPIError(err, "bitable", tableResult.Table)+"; falling back to create-only for this record") - created, createErr := client.CreateBitableRecord(context.Background(), token.Value, opts.BaseAppToken, tableID, record.Fields) + created, createErr := client.CreateBitableRecord(context.Background(), token.Value, opts.BaseAppToken, tableID, fields) if createErr != nil { result.Action = "create" result.Error = diagnoseOpenAPIError(createErr, "bitable", tableResult.Table) @@ -137,7 +138,7 @@ func syncBitableOrPreview(ctx *common.RuntimeContext, opts BitableSyncOptions, r continue } if search.Found { - updated, err := client.UpdateBitableRecord(context.Background(), token.Value, opts.BaseAppToken, tableID, search.RecordID, record.Fields) + updated, err := client.UpdateBitableRecord(context.Background(), token.Value, opts.BaseAppToken, tableID, search.RecordID, fields) if err != nil { result.Action = "update" result.RecordID = redactToken(search.RecordID) @@ -150,7 +151,7 @@ func syncBitableOrPreview(ctx *common.RuntimeContext, opts BitableSyncOptions, r result.RecordID = redactToken(updated.RecordID) output.Tables[i].Updated++ } else { - created, err := client.CreateBitableRecord(context.Background(), token.Value, opts.BaseAppToken, tableID, record.Fields) + created, err := client.CreateBitableRecord(context.Background(), token.Value, opts.BaseAppToken, tableID, fields) if err != nil { result.Action = "create" result.Error = diagnoseOpenAPIError(err, "bitable", tableResult.Table) @@ -168,6 +169,25 @@ func syncBitableOrPreview(ctx *common.RuntimeContext, opts BitableSyncOptions, r return renderBitableSyncOutput(os.Stdout, output, formatOrDefault(ctx, "json")) } +func normalizeBitableWriteFields(fields map[string]interface{}) map[string]interface{} { + normalized := make(map[string]interface{}, len(fields)) + for key, value := range fields { + switch typed := value.(type) { + case []string: + normalized[key] = strings.Join(typed, "\n") + case []interface{}: + parts := make([]string, 0, len(typed)) + for _, item := range typed { + parts = append(parts, fmt.Sprint(item)) + } + normalized[key] = strings.Join(parts, "\n") + default: + normalized[key] = value + } + } + return normalized +} + func renderBitableSyncOutput(w io.Writer, output BitableSyncOutput, format string) error { switch normalizeFormat(format) { case "markdown": diff --git a/shortcuts/feishu/card.go b/shortcuts/feishu/card.go index 7ca41f6..f3ecb4a 100644 --- a/shortcuts/feishu/card.go +++ b/shortcuts/feishu/card.go @@ -25,23 +25,23 @@ func NewInteractivePayload(card Card) WebhookPayload { } func BuildBotTestCard(title, message, lang string) Card { - title = firstNonEmpty(title, "GitLink Feishu integration test") - message = firstNonEmpty(message, "gitlink-cli can build and send Feishu custom bot cards.") + title = firstNonEmpty(title, feishuLabel(lang, "bot_title")) + message = firstNonEmpty(message, feishuLabel(lang, "bot_message")) return baseCard(title, "blue", []interface{}{ - div("**Status**\nReady"), + div(fmt.Sprintf("**%s**\n%s", feishuLabel(lang, "bot_status"), feishuLabel(lang, "ready"))), div(message), - note("Generated by gitlink-cli feishu +bot-test."), + note(feishuLabel(lang, "bot_generated")), }) } func BuildWorkflowCard(report workflow.RepoReportResult, include []string, title string, lang string, docURL string) Card { title = firstNonEmpty(title, reportTitle(report, lang)) elements := []interface{}{ - div(fmt.Sprintf("**Repository**\n%s", escapeMD(report.Repository))), + div(fmt.Sprintf("**%s**\n%s", feishuLabel(lang, "repository"), escapeMD(report.Repository))), fields([]fieldValue{ - {Label: "Report score", Value: fmt.Sprintf("%d", report.ReportScore)}, - {Label: "Risk level", Value: report.RiskLevel}, - {Label: "Source", Value: report.Source}, + {Label: feishuLabel(lang, "report_score"), Value: fmt.Sprintf("%d", report.ReportScore)}, + {Label: feishuLabel(lang, "risk_level"), Value: report.RiskLevel}, + {Label: feishuLabel(lang, "source"), Value: report.Source}, }), } if hasItem(include, "health") { @@ -52,41 +52,38 @@ func BuildWorkflowCard(report workflow.RepoReportResult, include []string, title healthRisk = report.Health.RiskLevel } elements = append(elements, fields([]fieldValue{ - {Label: "Health score", Value: healthScore}, - {Label: "Health risk", Value: healthRisk}, + {Label: feishuLabel(lang, "health_score"), Value: healthScore}, + {Label: feishuLabel(lang, "health_risk"), Value: healthRisk}, })) } if hasItem(include, "issues") { elements = append(elements, fields([]fieldValue{ - {Label: "Issues", Value: fmt.Sprintf("%d", report.IssueSummary.Total)}, - {Label: "High risk issues", Value: fmt.Sprintf("%d", report.IssueSummary.HighRisk)}, - {Label: "Missing info", Value: fmt.Sprintf("%d", report.IssueSummary.MissingInfo)}, + {Label: feishuLabel(lang, "issues"), Value: fmt.Sprintf("%d", report.IssueSummary.Total)}, + {Label: feishuLabel(lang, "high_risk_issues"), Value: fmt.Sprintf("%d", report.IssueSummary.HighRisk)}, + {Label: feishuLabel(lang, "missing_info"), Value: fmt.Sprintf("%d", report.IssueSummary.MissingInfo)}, })) } if hasItem(include, "prs") { elements = append(elements, fields([]fieldValue{ - {Label: "Pull requests", Value: fmt.Sprintf("%d", report.PRSummary.Total)}, - {Label: "High risk PRs", Value: fmt.Sprintf("%d", report.PRSummary.HighRisk)}, + {Label: feishuLabel(lang, "pull_requests"), Value: fmt.Sprintf("%d", report.PRSummary.Total)}, + {Label: feishuLabel(lang, "high_risk_prs"), Value: fmt.Sprintf("%d", report.PRSummary.HighRisk)}, })) if len(report.PRSummary.ReviewFocus) > 0 { - elements = append(elements, div("**Review focus**\n"+bulletList(report.PRSummary.ReviewFocus, 4))) + elements = append(elements, div(fmt.Sprintf("**%s**\n%s", feishuLabel(lang, "review_focus"), bulletList(localizeFeishuLines(report.PRSummary.ReviewFocus, lang), 4)))) } } if len(report.Recommendations) > 0 { - elements = append(elements, div("**Recommendations**\n"+bulletList(report.Recommendations, 5))) + elements = append(elements, div(fmt.Sprintf("**%s**\n%s", feishuLabel(lang, "recommendations"), bulletList(localizeFeishuLines(report.Recommendations, lang), 5)))) } if strings.TrimSpace(docURL) != "" { - elements = append(elements, actionButton("Open Feishu report", docURL)) + elements = append(elements, actionButton(feishuLabel(lang, "open_feishu_report"), docURL)) } - elements = append(elements, note("Preview is read-only. Bitable records are generated locally by +bitable-records.")) + elements = append(elements, note(feishuLabel(lang, "preview_note"))) return baseCard(title, templateForRisk(report.RiskLevel), elements) } func reportTitle(report workflow.RepoReportResult, lang string) string { - if lang == "zh-CN" { - return "GitLink workflow report: " + report.Repository - } - return "GitLink workflow report: " + report.Repository + return fmt.Sprintf(feishuLabel(lang, "workflow_report_title"), report.Repository) } func baseCard(title string, template string, elements []interface{}) Card { diff --git a/shortcuts/feishu/digest.go b/shortcuts/feishu/digest.go index a937bc2..967f990 100644 --- a/shortcuts/feishu/digest.go +++ b/shortcuts/feishu/digest.go @@ -126,49 +126,49 @@ func BuildContributorDigest(report workflow.RepoReportResult, docURL string) Rol } } -func BuildOwnerDigestCard(digest RoleDigest, title string, _ string) Card { - return buildDigestCard(digest, firstNonEmpty(title, "GitLink owner digest: "+digest.Repository), "owner") +func BuildOwnerDigestCard(digest RoleDigest, title string, lang string) Card { + return buildDigestCard(digest, firstNonEmpty(title, fmt.Sprintf(feishuLabel(lang, "owner_digest_title"), digest.Repository)), "owner", lang) } -func BuildContributorDigestCard(digest RoleDigest, title string, _ string) Card { - return buildDigestCard(digest, firstNonEmpty(title, "GitLink contributor digest: "+digest.Repository), "contributor") +func BuildContributorDigestCard(digest RoleDigest, title string, lang string) Card { + return buildDigestCard(digest, firstNonEmpty(title, fmt.Sprintf(feishuLabel(lang, "contributor_digest_title"), digest.Repository)), "contributor", lang) } -func buildDigestCard(digest RoleDigest, title string, role string) Card { +func buildDigestCard(digest RoleDigest, title string, role string, lang string) Card { elements := []interface{}{ - div(fmt.Sprintf("**Repository**\n%s", escapeMD(digest.Repository))), + div(fmt.Sprintf("**%s**\n%s", feishuLabel(lang, "repository"), escapeMD(digest.Repository))), fields([]fieldValue{ - {Label: "Report score", Value: fmt.Sprintf("%d", digest.ReportScore)}, - {Label: "Risk level", Value: digest.RiskLevel}, - {Label: "Issues", Value: fmt.Sprintf("%d", digest.IssueTotal)}, - {Label: "Pull requests", Value: fmt.Sprintf("%d", digest.PRTotal)}, + {Label: feishuLabel(lang, "report_score"), Value: fmt.Sprintf("%d", digest.ReportScore)}, + {Label: feishuLabel(lang, "risk_level"), Value: digest.RiskLevel}, + {Label: feishuLabel(lang, "issues"), Value: fmt.Sprintf("%d", digest.IssueTotal)}, + {Label: feishuLabel(lang, "pull_requests"), Value: fmt.Sprintf("%d", digest.PRTotal)}, }), fields([]fieldValue{ - {Label: "High-risk issues", Value: fmt.Sprintf("%d", digest.IssueHighRisk)}, - {Label: "Missing-info issues", Value: fmt.Sprintf("%d", digest.IssueMissingInfo)}, - {Label: "High-risk PRs", Value: fmt.Sprintf("%d", digest.PRHighRisk)}, - {Label: "Review focus", Value: fmt.Sprintf("%d", len(digest.ReviewFocus))}, + {Label: feishuLabel(lang, "high_risk_issues"), Value: fmt.Sprintf("%d", digest.IssueHighRisk)}, + {Label: feishuLabel(lang, "missing_info_issues"), Value: fmt.Sprintf("%d", digest.IssueMissingInfo)}, + {Label: feishuLabel(lang, "high_risk_prs"), Value: fmt.Sprintf("%d", digest.PRHighRisk)}, + {Label: feishuLabel(lang, "review_focus"), Value: fmt.Sprintf("%d", len(digest.ReviewFocus))}, }), } if digest.HealthScore != nil { elements = append(elements, fields([]fieldValue{ - {Label: "Health score", Value: fmt.Sprintf("%d", *digest.HealthScore)}, - {Label: "Health risk", Value: digest.HealthRisk}, + {Label: feishuLabel(lang, "health_score"), Value: fmt.Sprintf("%d", *digest.HealthScore)}, + {Label: feishuLabel(lang, "health_risk"), Value: digest.HealthRisk}, })) } if len(digest.AttentionItems) > 0 { - elements = append(elements, div("**Attention**\n"+bulletList(digest.AttentionItems, 5))) + elements = append(elements, div(fmt.Sprintf("**%s**\n%s", feishuLabel(lang, "attention"), bulletList(localizeFeishuLines(digest.AttentionItems, lang), 5)))) } if len(digest.NextSteps) > 0 { - elements = append(elements, div("**Suggested next steps**\n"+bulletList(digest.NextSteps, 5))) + elements = append(elements, div(fmt.Sprintf("**%s**\n%s", feishuLabel(lang, "suggested_next_steps"), bulletList(localizeFeishuLines(digest.NextSteps, lang), 5)))) } if digest.RepositoryURL != "" { - elements = append(elements, actionButton("Open GitLink repository", digest.RepositoryURL)) + elements = append(elements, actionButton(feishuLabel(lang, "open_gitlink_repository"), digest.RepositoryURL)) } if digest.DocURL != "" { - elements = append(elements, actionButton("Open Feishu report", digest.DocURL)) + elements = append(elements, actionButton(feishuLabel(lang, "open_feishu_report"), digest.DocURL)) } - elements = append(elements, note(digest.BoundaryDescription)) + elements = append(elements, note(localizedBoundary(digest, lang))) template := templateForRisk(digest.RiskLevel) if role == "contributor" && digest.PRSummaryNeedsAttention() { template = "yellow" @@ -176,60 +176,121 @@ func buildDigestCard(digest RoleDigest, title string, role string) Card { return baseCard(title, template, elements) } +func localizedBoundary(digest RoleDigest, lang string) string { + if !isChineseLang(lang) { + return digest.BoundaryDescription + } + switch digest.Role { + case "owner": + return feishuLabel(lang, "boundary_owner") + case "contributor": + return feishuLabel(lang, "boundary_contributor") + default: + return localizeFeishuText(digest.BoundaryDescription, lang) + } +} + func (d RoleDigest) PRSummaryNeedsAttention() bool { return d.PRHighRisk > 0 || len(d.ReviewFocus) > 0 } -func renderDigest(w io.Writer, digest RoleDigest, format string) error { +func renderDigest(w io.Writer, digest RoleDigest, format string, lang string) error { switch normalizeFormat(format) { case "markdown": - return writeDigestMarkdown(w, digest) + return writeDigestMarkdown(w, digest, lang) case "table": - return writeDigestTable(w, digest) + return writeDigestTable(w, digest, lang) default: return writeJSON(w, digest) } } -func writeDigestMarkdown(w io.Writer, digest RoleDigest) error { - if _, err := fmt.Fprintf(w, "# GitLink %s digest: %s\n\n", digest.Role, digest.Repository); err != nil { +func writeDigestMarkdown(w io.Writer, digest RoleDigest, lang string) error { + title := fmt.Sprintf("# GitLink %s digest: %s\n\n", digest.Role, digest.Repository) + if isChineseLang(lang) { + role := "角色" + if digest.Role == "owner" { + role = "Owner" + } + if digest.Role == "contributor" { + role = "贡献者" + } + title = fmt.Sprintf("# GitLink %s摘要:%s\n\n", role, digest.Repository) + } + if _, err := fmt.Fprint(w, title); err != nil { return err } - lines := []string{ - fmt.Sprintf("- Report score: `%d`", digest.ReportScore), - fmt.Sprintf("- Risk level: `%s`", firstNonEmpty(digest.RiskLevel, "unknown")), - fmt.Sprintf("- Issues: `%d` total, `%d` high risk, `%d` missing info", digest.IssueTotal, digest.IssueHighRisk, digest.IssueMissingInfo), - fmt.Sprintf("- Pull requests: `%d` total, `%d` high risk", digest.PRTotal, digest.PRHighRisk), - } + lines := digestMarkdownLines(digest, lang) if digest.HealthScore != nil { - lines = append(lines, fmt.Sprintf("- Health score: `%d`; health risk: `%s`", *digest.HealthScore, firstNonEmpty(digest.HealthRisk, "unknown"))) + if isChineseLang(lang) { + lines = append(lines, fmt.Sprintf("- 健康分:`%d`;健康风险:`%s`", *digest.HealthScore, firstNonEmpty(digest.HealthRisk, "unknown"))) + } else { + lines = append(lines, fmt.Sprintf("- Health score: `%d`; health risk: `%s`", *digest.HealthScore, firstNonEmpty(digest.HealthRisk, "unknown"))) + } } if digest.RepositoryURL != "" { - lines = append(lines, "- GitLink repository: "+digest.RepositoryURL) + if isChineseLang(lang) { + lines = append(lines, "- GitLink 仓库:"+digest.RepositoryURL) + } else { + lines = append(lines, "- GitLink repository: "+digest.RepositoryURL) + } } if digest.DocURL != "" { - lines = append(lines, "- Feishu report: "+digest.DocURL) + if isChineseLang(lang) { + lines = append(lines, "- 飞书报告:"+digest.DocURL) + } else { + lines = append(lines, "- Feishu report: "+digest.DocURL) + } } if _, err := fmt.Fprintln(w, strings.Join(lines, "\n")); err != nil { return err } if len(digest.AttentionItems) > 0 { - if _, err := fmt.Fprint(w, "\n## Attention\n\n"+bulletList(digest.AttentionItems, 8)+"\n"); err != nil { + heading := "Attention" + if isChineseLang(lang) { + heading = "需要关注" + } + if _, err := fmt.Fprintf(w, "\n## %s\n\n%s\n", heading, bulletList(localizeFeishuLines(digest.AttentionItems, lang), 8)); err != nil { return err } } if len(digest.NextSteps) > 0 { - if _, err := fmt.Fprint(w, "\n## Suggested next steps\n\n"+bulletList(digest.NextSteps, 8)+"\n"); err != nil { + heading := "Suggested next steps" + if isChineseLang(lang) { + heading = "建议下一步" + } + if _, err := fmt.Fprintf(w, "\n## %s\n\n%s\n", heading, bulletList(localizeFeishuLines(digest.NextSteps, lang), 8)); err != nil { return err } } - _, err := fmt.Fprintf(w, "\n> %s\n", digest.BoundaryDescription) + _, err := fmt.Fprintf(w, "\n> %s\n", localizedBoundary(digest, lang)) return err } -func writeDigestTable(w io.Writer, digest RoleDigest) error { +func digestMarkdownLines(digest RoleDigest, lang string) []string { + if isChineseLang(lang) { + return []string{ + fmt.Sprintf("- 报告分数:`%d`", digest.ReportScore), + fmt.Sprintf("- 风险等级:`%s`", firstNonEmpty(digest.RiskLevel, "unknown")), + fmt.Sprintf("- Issue:总数 `%d`,高风险 `%d`,信息缺失 `%d`", digest.IssueTotal, digest.IssueHighRisk, digest.IssueMissingInfo), + fmt.Sprintf("- PR:总数 `%d`,高风险 `%d`", digest.PRTotal, digest.PRHighRisk), + } + } + return []string{ + fmt.Sprintf("- Report score: `%d`", digest.ReportScore), + fmt.Sprintf("- Risk level: `%s`", firstNonEmpty(digest.RiskLevel, "unknown")), + fmt.Sprintf("- Issues: `%d` total, `%d` high risk, `%d` missing info", digest.IssueTotal, digest.IssueHighRisk, digest.IssueMissingInfo), + fmt.Sprintf("- Pull requests: `%d` total, `%d` high risk", digest.PRTotal, digest.PRHighRisk), + } +} + +func writeDigestTable(w io.Writer, digest RoleDigest, lang string) error { tw := tabwriter.NewWriter(w, 0, 0, 2, ' ', 0) - if _, err := fmt.Fprintln(tw, "ROLE\tREPOSITORY\tRISK\tSCORE\tISSUES\tHIGH_RISK_ISSUES\tPRS\tHIGH_RISK_PRS\tATTENTION"); err != nil { + header := "ROLE\tREPOSITORY\tRISK\tSCORE\tISSUES\tHIGH_RISK_ISSUES\tPRS\tHIGH_RISK_PRS\tATTENTION" + if isChineseLang(lang) { + header = "角色\t仓库\t风险\t分数\tIssue\t高风险Issue\tPR\t高风险PR\t关注项" + } + if _, err := fmt.Fprintln(tw, header); err != nil { return err } if _, err := fmt.Fprintf(tw, "%s\t%s\t%s\t%d\t%d\t%d\t%d\t%d\t%d\n", diff --git a/shortcuts/feishu/doc_export.go b/shortcuts/feishu/doc_export.go index bb71cbd..0eb6452 100644 --- a/shortcuts/feishu/doc_export.go +++ b/shortcuts/feishu/doc_export.go @@ -183,23 +183,23 @@ func BuildDocBlocks(report workflow.RepoReportResult, lang string) []DocBlock { healthRisk = report.Health.RiskLevel } blocks := []DocBlock{ - textBlock("GitLink workflow report: " + report.Repository), - textBlock(fmt.Sprintf("Report score: %d", report.ReportScore)), - textBlock("Risk level: " + firstNonEmpty(report.RiskLevel, "unknown")), - textBlock(fmt.Sprintf("Health score: %s; health risk: %s", healthScore, healthRisk)), - textBlock(fmt.Sprintf("Issues: total=%d, high_risk=%d, missing_info=%d", report.IssueSummary.Total, report.IssueSummary.HighRisk, report.IssueSummary.MissingInfo)), - textBlock(fmt.Sprintf("Pull Requests: total=%d, high_risk=%d", report.PRSummary.Total, report.PRSummary.HighRisk)), + textBlock(fmt.Sprintf(feishuLabel(lang, "doc_title"), report.Repository)), + textBlock(fmt.Sprintf(feishuLabel(lang, "doc_report_score"), report.ReportScore)), + textBlock(fmt.Sprintf(feishuLabel(lang, "doc_risk"), firstNonEmpty(report.RiskLevel, "unknown"))), + textBlock(fmt.Sprintf(feishuLabel(lang, "doc_health"), healthScore, healthRisk)), + textBlock(fmt.Sprintf(feishuLabel(lang, "doc_issues"), report.IssueSummary.Total, report.IssueSummary.HighRisk, report.IssueSummary.MissingInfo)), + textBlock(fmt.Sprintf(feishuLabel(lang, "doc_prs"), report.PRSummary.Total, report.PRSummary.HighRisk)), } if len(report.PRSummary.ReviewFocus) > 0 { - blocks = append(blocks, textBlock("Review focus:\n"+joinLines(report.PRSummary.ReviewFocus, 6))) + blocks = append(blocks, textBlock(feishuLabel(lang, "doc_review_focus")+":\n"+joinLines(localizeFeishuLines(report.PRSummary.ReviewFocus, lang), 6))) } if len(report.Recommendations) > 0 { - blocks = append(blocks, textBlock("Recommendations:\n"+joinLines(report.Recommendations, 8))) + blocks = append(blocks, textBlock(feishuLabel(lang, "doc_recommendations")+":\n"+joinLines(localizeFeishuLines(report.Recommendations, lang), 8))) } if len(report.Reasoning) > 0 { - blocks = append(blocks, textBlock("Reasoning:\n"+joinLines(report.Reasoning, 8))) + blocks = append(blocks, textBlock(feishuLabel(lang, "doc_reasoning")+":\n"+joinLines(localizeFeishuLines(report.Reasoning, lang), 8))) } - blocks = append(blocks, textBlock("Source: "+firstNonEmpty(report.Source, "workflow-json"))) + blocks = append(blocks, textBlock(fmt.Sprintf(feishuLabel(lang, "doc_source"), firstNonEmpty(report.Source, "workflow-json")))) return blocks } diff --git a/shortcuts/feishu/feishu.go b/shortcuts/feishu/feishu.go index 413e47f..6ca5ad7 100644 --- a/shortcuts/feishu/feishu.go +++ b/shortcuts/feishu/feishu.go @@ -279,7 +279,7 @@ func runOwnerDigest(ctx *common.RuntimeContext) error { title := firstNonEmpty(ctx.Arg("title"), "GitLink owner digest: "+report.Repository) return deliverOrPreview(ctx, opts, NewInteractivePayload(BuildOwnerDigestCard(digest, title, normalizeLang(ctx.Arg("lang")))), "") } - return renderDigest(os.Stdout, digest, formatOrDefault(ctx, "markdown")) + return renderDigest(os.Stdout, digest, formatOrDefault(ctx, "markdown"), normalizeLang(ctx.Arg("lang"))) } func runContributorDigest(ctx *common.RuntimeContext) error { @@ -296,7 +296,7 @@ func runContributorDigest(ctx *common.RuntimeContext) error { title := firstNonEmpty(ctx.Arg("title"), "GitLink contributor digest: "+report.Repository) return deliverOrPreview(ctx, opts, NewInteractivePayload(BuildContributorDigestCard(digest, title, normalizeLang(ctx.Arg("lang")))), "") } - return renderDigest(os.Stdout, digest, formatOrDefault(ctx, "markdown")) + return renderDigest(os.Stdout, digest, formatOrDefault(ctx, "markdown"), normalizeLang(ctx.Arg("lang"))) } func runDocExport(ctx *common.RuntimeContext) error { @@ -343,8 +343,8 @@ func runTaskPreview(ctx *common.RuntimeContext) error { if err != nil { return err } - tasks := BuildTaskCandidates(report, ctx.Arg("doc-url")) - return renderTaskOutput(os.Stdout, TaskOutput{Mode: "preview", DryRun: true, Tasks: tasks}, formatOrDefault(ctx, "markdown")) + tasks := BuildTaskCandidatesLocalized(report, ctx.Arg("doc-url"), normalizeLang(ctx.Arg("lang"))) + return renderTaskOutput(os.Stdout, taskPreviewOutput(tasks), formatOrDefault(ctx, "markdown")) } func runTaskCreate(ctx *common.RuntimeContext) error { @@ -356,7 +356,7 @@ func runTaskCreate(ctx *common.RuntimeContext) error { if err != nil { return err } - tasks := BuildTaskCandidates(report, ctx.Arg("doc-url")) + tasks := BuildTaskCandidatesLocalized(report, ctx.Arg("doc-url"), normalizeLang(ctx.Arg("lang"))) return createTasksOrPreview(ctx, opts, tasks) } diff --git a/shortcuts/feishu/feishu_test.go b/shortcuts/feishu/feishu_test.go index ae69dfb..666216a 100644 --- a/shortcuts/feishu/feishu_test.go +++ b/shortcuts/feishu/feishu_test.go @@ -221,6 +221,21 @@ func TestTaskCandidatesAreStable(t *testing.T) { } } +func TestTaskPreviewOutputCountsTasks(t *testing.T) { + report := workflowReportFixture(t) + tasks := BuildTaskCandidates(report, "") + output := taskPreviewOutput(tasks) + if output.TaskCount != len(tasks) { + t.Fatalf("TaskCount = %d, want %d", output.TaskCount, len(tasks)) + } + if output.Send { + t.Fatal("preview output must not be marked as send") + } + if !output.DryRun { + t.Fatal("preview output must be dry-run") + } +} + func TestBitableSyncOptionsRejectSendDryRun(t *testing.T) { ctx := &common.RuntimeContext{Args: map[string]string{ "send": "true", @@ -234,6 +249,24 @@ func TestBitableSyncOptionsRejectSendDryRun(t *testing.T) { } } +func TestNormalizeBitableWriteFieldsFlattensStringSlices(t *testing.T) { + fields := normalizeBitableWriteFields(map[string]interface{}{ + "unique_key": "issue:test", + "recommended_action": []string{"first", "second"}, + "review_focus": []interface{}{"focus-a", "focus-b"}, + "count": 2, + }) + if fields["recommended_action"] != "first\nsecond" { + t.Fatalf("recommended_action = %#v", fields["recommended_action"]) + } + if fields["review_focus"] != "focus-a\nfocus-b" { + t.Fatalf("review_focus = %#v", fields["review_focus"]) + } + if fields["count"] != 2 { + t.Fatalf("count changed: %#v", fields["count"]) + } +} + func TestBitableSyncMockHTTP(t *testing.T) { report := workflowReportFixture(t) records := BuildBitableRecords(report, []string{"reports"}, "") @@ -344,6 +377,23 @@ func TestTaskCreateMockHTTP(t *testing.T) { } } +func TestTaskCreateTableShowsResults(t *testing.T) { + var out strings.Builder + output := TaskOutput{Results: []TaskCreateResult{{ + UniqueKey: "task:test", + Title: "Review report", + TaskID: "task_guid_123456", + Created: true, + }}} + if err := renderTaskOutput(&out, output, "table"); err != nil { + t.Fatalf("renderTaskOutput returned error: %v", err) + } + rendered := out.String() + if !strings.Contains(rendered, "CREATED") || !strings.Contains(rendered, "task...3456") { + t.Fatalf("task table did not show result details: %s", rendered) + } +} + func TestWikiNodeTokenFromURL(t *testing.T) { got := wikiNodeTokenFromURL("https://tenant.feishu.cn/wiki/NodeToken123?from=from_copylink") if got != "NodeToken123" { diff --git a/shortcuts/feishu/l10n.go b/shortcuts/feishu/l10n.go new file mode 100644 index 0000000..f7d0022 --- /dev/null +++ b/shortcuts/feishu/l10n.go @@ -0,0 +1,181 @@ +package feishu + +import ( + "fmt" + "regexp" + "strings" +) + +func isChineseLang(lang string) bool { + return strings.EqualFold(strings.TrimSpace(lang), "zh-CN") +} + +func feishuLabel(lang string, key string) string { + if !isChineseLang(lang) { + return feishuLabelsEN[key] + } + if value := feishuLabelsZH[key]; value != "" { + return value + } + return feishuLabelsEN[key] +} + +func localizeFeishuText(value string, lang string) string { + value = strings.TrimSpace(value) + if value == "" || !isChineseLang(lang) { + return value + } + if translated := knownFeishuTranslations[value]; translated != "" { + return translated + } + for _, pattern := range knownFeishuPatterns { + if match := pattern.re.FindStringSubmatch(value); len(match) > 1 { + return fmt.Sprintf(pattern.format, match[1]) + } + } + return value +} + +func localizeFeishuLines(values []string, lang string) []string { + if !isChineseLang(lang) { + return values + } + result := make([]string, 0, len(values)) + for _, value := range values { + result = append(result, localizeFeishuText(value, lang)) + } + return result +} + +var feishuLabelsEN = map[string]string{ + "attention": "Attention", + "boundary_contributor": "Contributor digest is role-oriented, not personalized. It does not use Feishu open_id or union_id routing.", + "boundary_owner": "Owner digest is a read-only summary. It does not modify GitLink or Feishu resources.", + "bot_generated": "Generated by gitlink-cli feishu +bot-test.", + "bot_message": "gitlink-cli can build and send Feishu custom bot cards.", + "bot_status": "Status", + "bot_title": "GitLink Feishu integration test", + "doc_health": "Health score: %s; health risk: %s", + "doc_issues": "Issues: total=%d, high_risk=%d, missing_info=%d", + "doc_prs": "Pull Requests: total=%d, high_risk=%d", + "doc_reasoning": "Reasoning", + "doc_recommendations": "Recommendations", + "doc_report_score": "Report score: %d", + "doc_review_focus": "Review focus", + "doc_risk": "Risk level: %s", + "doc_source": "Source: %s", + "doc_title": "GitLink workflow report: %s", + "health_risk": "Health risk", + "health_score": "Health score", + "high_risk_issues": "High-risk issues", + "high_risk_prs": "High-risk PRs", + "issues": "Issues", + "missing_info": "Missing info", + "missing_info_issues": "Missing-info issues", + "open_feishu_report": "Open Feishu report", + "open_gitlink_repository": "Open GitLink repository", + "owner_digest_title": "GitLink owner digest: %s", + "contributor_digest_title": "GitLink contributor digest: %s", + "preview_note": "Preview is read-only. Bitable records are generated locally by +bitable-records.", + "pull_requests": "Pull requests", + "ready": "Ready", + "recommendations": "Recommendations", + "report_score": "Report score", + "repository": "Repository", + "review_focus": "Review focus", + "risk_level": "Risk level", + "source": "Source", + "suggested_next_steps": "Suggested next steps", + "task_description_default": "Workflow recommendation from gitlink-cli repo report.", + "task_missing_info_desc": "Some issues need reproduction steps, logs, version details, or command output.", + "task_pr_high_risk_desc": "High-risk PR bucket from workflow report. Check review focus and merge readiness.", + "task_review_focus_desc": "Review focus items from the workflow report.", + "task_review_report_desc": "No high-risk task candidates were detected. Keep a regular owner review cadence.", + "workflow_report_title": "GitLink workflow report: %s", +} + +var feishuLabelsZH = map[string]string{ + "attention": "需要关注", + "boundary_contributor": "贡献者摘要是按角色生成的汇总,不是基于飞书 open_id 或 union_id 的个人定向推送。", + "boundary_owner": "Owner 摘要是只读汇总,不会修改 GitLink 或飞书资源。", + "bot_generated": "由 gitlink-cli feishu +bot-test 生成。", + "bot_message": "gitlink-cli 可以构建并发送飞书自定义机器人卡片。", + "bot_status": "状态", + "bot_title": "GitLink 飞书集成测试", + "doc_health": "健康分:%s;健康风险:%s", + "doc_issues": "Issue:总数=%d,高风险=%d,信息缺失=%d", + "doc_prs": "PR:总数=%d,高风险=%d", + "doc_reasoning": "判断依据", + "doc_recommendations": "建议操作", + "doc_report_score": "报告分数:%d", + "doc_review_focus": "审查重点", + "doc_risk": "风险等级:%s", + "doc_source": "来源:%s", + "doc_title": "GitLink 工作流报告:%s", + "health_risk": "健康风险", + "health_score": "健康分", + "high_risk_issues": "高风险 Issue", + "high_risk_prs": "高风险 PR", + "issues": "Issue", + "missing_info": "信息缺失", + "missing_info_issues": "信息缺失 Issue", + "open_feishu_report": "打开飞书报告", + "open_gitlink_repository": "打开 GitLink 仓库", + "owner_digest_title": "GitLink Owner 摘要:%s", + "contributor_digest_title": "GitLink 贡献者摘要:%s", + "preview_note": "当前为只读预览。多维表格记录由 +bitable-records 在本地生成。", + "pull_requests": "PR", + "ready": "就绪", + "recommendations": "建议操作", + "report_score": "报告分数", + "repository": "仓库", + "review_focus": "审查重点", + "risk_level": "风险等级", + "source": "来源", + "suggested_next_steps": "建议下一步", + "task_description_default": "来自 gitlink-cli 仓库报告的工作流建议。", + "task_missing_info_desc": "部分 Issue 需要补充复现步骤、日志、版本信息或命令输出。", + "task_pr_high_risk_desc": "工作流报告识别到高风险 PR,请检查审查重点和合并准备状态。", + "task_review_focus_desc": "来自工作流报告的 PR 审查重点。", + "task_review_report_desc": "未识别到高风险任务候选,建议保持定期 owner 复查节奏。", + "workflow_report_title": "GitLink 工作流报告:%s", +} + +var knownFeishuTranslations = map[string]string{ + "Add LICENSE and CONTRIBUTING files for contributor clarity.": "补充 LICENSE 和 CONTRIBUTING,降低贡献者理解成本。", + "Add missing reproduction steps, logs, or screenshots when requested.": "按需补充复现步骤、日志或截图。", + "Add or improve README and contribution guidance.": "补充或改进 README 与贡献指南。", + "Check PR review focus and update the related branch or description.": "检查 PR 审查重点,并更新相关分支或描述。", + "Keep GitLink write actions outside this digest; card buttons are navigation-only.": "此摘要不执行 GitLink 写操作;卡片按钮仅用于跳转。", + "Maintain the current workflow and review the repository report regularly.": "保持当前维护节奏,并定期复查仓库报告。", + "No contributor-specific blocker was detected in the workflow report.": "工作流报告未识别到明确的贡献者阻塞项。", + "No critical owner action was detected in the workflow report.": "工作流报告未识别到紧急 owner 动作。", + "Open the GitLink repository or Feishu report link for details.": "打开 GitLink 仓库或飞书报告查看详情。", + "Prioritize high or critical risk pull requests.": "优先审阅 high / critical 风险 PR。", + "Prioritize high-risk pull request feedback before new work.": "先处理高风险 PR 反馈,再开始新工作。", + "Reduce stale issues and add response labels or next actions.": "减少长期未处理的 Issue,并补充响应标签或下一步动作。", + "Request missing information for 3 issues": "为 3 个 Issue 补充缺失信息", + "Request missing reproduction steps, version, command output, or logs.": "要求补充复现步骤、版本、命令输出或日志。", + "Review PR focus areas": "审查 PR 重点区域", + "Review high-risk issues and PRs first.": "优先处理高风险 Issue 和 PR。", + "Review report risks and schedule the next maintenance actions.": "复查报告中的风险项,并安排下一步维护动作。", + "Review stale pull requests and clarify merge blockers.": "审查长期未处理的 PR,并明确合并阻塞点。", + "Use the Feishu report document for full context when available.": "如有飞书报告文档,优先查看完整上下文。", + "Use the health recommendations to reduce repository governance risk.": "根据健康度建议降低仓库治理风险。", + "Workflow recommendation from gitlink-cli repo report.": "来自 gitlink-cli 仓库报告的工作流建议。", +} + +var knownFeishuPatterns = []struct { + re *regexp.Regexp + format string +}{ + {regexp.MustCompile(`^(\d+) high-risk issues need maintainer triage$`), "%s 个高风险 Issue 需要维护者分诊"}, + {regexp.MustCompile(`^(\d+) issues are missing required information$`), "%s 个 Issue 缺少必要信息"}, + {regexp.MustCompile(`^(\d+) high-risk pull requests need owner review$`), "%s 个高风险 PR 需要 owner 审阅"}, + {regexp.MustCompile(`^repository health score is (\d+)$`), "仓库健康分为 %s"}, + {regexp.MustCompile(`^(\d+) high-risk pull requests may need contributor updates$`), "%s 个高风险 PR 可能需要贡献者更新"}, + {regexp.MustCompile(`^(\d+) issues need clearer reproduction details or missing information$`), "%s 个 Issue 需要更清晰的复现信息或缺失信息"}, + {regexp.MustCompile(`^(\d+) high-risk issues may need focused follow-up$`), "%s 个高风险 Issue 需要重点跟进"}, + {regexp.MustCompile(`^Request missing information for (\d+) issues$`), "为 %s 个 Issue 补充缺失信息"}, + {regexp.MustCompile(`^Review (\d+) high-risk pull requests$`), "审查 %s 个高风险 PR"}, +} diff --git a/shortcuts/feishu/task.go b/shortcuts/feishu/task.go index 2a06b91..037d505 100644 --- a/shortcuts/feishu/task.go +++ b/shortcuts/feishu/task.go @@ -58,14 +58,22 @@ type TaskCreateResult struct { } func BuildTaskCandidates(report workflow.RepoReportResult, docURL string) []TaskCandidate { + return buildTaskCandidates(report, docURL, defaultLang) +} + +func BuildTaskCandidatesLocalized(report workflow.RepoReportResult, docURL string, lang string) []TaskCandidate { + return buildTaskCandidates(report, docURL, lang) +} + +func buildTaskCandidates(report workflow.RepoReportResult, docURL string, lang string) []TaskCandidate { tasks := []TaskCandidate{} repoURL := gitlinkRepoURL(report.Repository) for i, recommendation := range report.Recommendations { - title := firstNonEmpty(recommendation, "Review workflow recommendation") + title := firstNonEmpty(localizeFeishuText(recommendation, lang), "Review workflow recommendation") tasks = append(tasks, TaskCandidate{ UniqueKey: stableKey("task", report.Repository, "recommendation", fmt.Sprintf("%d", i+1)), Title: title, - Description: "Workflow recommendation from gitlink-cli repo report.", + Description: feishuLabel(lang, "task_description_default"), SourceType: "recommendation", SourceKey: fmt.Sprintf("recommendation-%d", i+1), Repository: report.Repository, @@ -80,8 +88,8 @@ func BuildTaskCandidates(report workflow.RepoReportResult, docURL string) []Task if report.IssueSummary.HighRisk > 0 { tasks = append(tasks, TaskCandidate{ UniqueKey: stableKey("task", report.Repository, "issues", "high-risk"), - Title: fmt.Sprintf("Triage %d high-risk GitLink issues", report.IssueSummary.HighRisk), - Description: "High-risk issue bucket from workflow report. Review GitLink issues before routine work.", + Title: localizeFeishuText(fmt.Sprintf("Triage %d high-risk GitLink issues", report.IssueSummary.HighRisk), lang), + Description: localizeFeishuText("High-risk issue bucket from workflow report. Review GitLink issues before routine work.", lang), SourceType: "issues", SourceKey: "issues-high-risk", Repository: report.Repository, @@ -96,8 +104,8 @@ func BuildTaskCandidates(report workflow.RepoReportResult, docURL string) []Task if report.IssueSummary.MissingInfo > 0 { tasks = append(tasks, TaskCandidate{ UniqueKey: stableKey("task", report.Repository, "issues", "missing-info"), - Title: fmt.Sprintf("Request missing information for %d issues", report.IssueSummary.MissingInfo), - Description: "Some issues need reproduction steps, logs, version details, or command output.", + Title: localizeFeishuText(fmt.Sprintf("Request missing information for %d issues", report.IssueSummary.MissingInfo), lang), + Description: feishuLabel(lang, "task_missing_info_desc"), SourceType: "issues", SourceKey: "issues-missing-info", Repository: report.Repository, @@ -112,8 +120,8 @@ func BuildTaskCandidates(report workflow.RepoReportResult, docURL string) []Task if report.PRSummary.HighRisk > 0 { tasks = append(tasks, TaskCandidate{ UniqueKey: stableKey("task", report.Repository, "prs", "high-risk"), - Title: fmt.Sprintf("Review %d high-risk pull requests", report.PRSummary.HighRisk), - Description: "High-risk PR bucket from workflow report. Check review focus and merge readiness.", + Title: localizeFeishuText(fmt.Sprintf("Review %d high-risk pull requests", report.PRSummary.HighRisk), lang), + Description: feishuLabel(lang, "task_pr_high_risk_desc"), SourceType: "prs", SourceKey: "prs-high-risk", Repository: report.Repository, @@ -128,8 +136,8 @@ func BuildTaskCandidates(report workflow.RepoReportResult, docURL string) []Task if len(report.PRSummary.ReviewFocus) > 0 { tasks = append(tasks, TaskCandidate{ UniqueKey: stableKey("task", report.Repository, "prs", "review-focus"), - Title: "Review PR focus areas", - Description: strings.Join(limitStrings(report.PRSummary.ReviewFocus, 8), "\n"), + Title: localizeFeishuText("Review PR focus areas", lang), + Description: strings.Join(limitStrings(localizeFeishuLines(report.PRSummary.ReviewFocus, lang), 8), "\n"), SourceType: "prs", SourceKey: "prs-review-focus", Repository: report.Repository, @@ -144,8 +152,8 @@ func BuildTaskCandidates(report workflow.RepoReportResult, docURL string) []Task if len(tasks) == 0 { tasks = append(tasks, TaskCandidate{ UniqueKey: stableKey("task", report.Repository, "report", "review"), - Title: "Review GitLink workflow report", - Description: "No high-risk task candidates were detected. Keep a regular owner review cadence.", + Title: localizeFeishuText("Review GitLink workflow report", lang), + Description: feishuLabel(lang, "task_review_report_desc"), SourceType: "report", SourceKey: "report-review", Repository: report.Repository, @@ -160,6 +168,15 @@ func BuildTaskCandidates(report workflow.RepoReportResult, docURL string) []Task return dedupeTasks(tasks) } +func taskPreviewOutput(tasks []TaskCandidate) TaskOutput { + return TaskOutput{ + Mode: "preview", + DryRun: true, + TaskCount: len(tasks), + Tasks: tasks, + } +} + func taskCreateOptionsFromContext(ctx *common.RuntimeContext) (TaskCreateOptions, error) { opts := TaskCreateOptions{ AppID: firstNonEmpty(ctx.Arg("app-id"), os.Getenv("FEISHU_APP_ID")), @@ -262,6 +279,17 @@ func writeTaskMarkdown(w io.Writer, output TaskOutput) error { func writeTaskTable(w io.Writer, output TaskOutput) error { tw := tabwriter.NewWriter(w, 0, 0, 2, ' ', 0) + if len(output.Results) > 0 { + if _, err := fmt.Fprintln(tw, "KEY\tCREATED\tTASK_ID\tTITLE\tERROR"); err != nil { + return err + } + for _, result := range output.Results { + if _, err := fmt.Fprintf(tw, "%s\t%t\t%s\t%s\t%s\n", result.UniqueKey, result.Created, redactToken(result.TaskID), result.Title, result.Error); err != nil { + return err + } + } + return tw.Flush() + } if _, err := fmt.Fprintln(tw, "KEY\tPRIORITY\tSOURCE\tTITLE"); err != nil { return err }