feat(feishu): validate real exports and zh-CN output

This commit is contained in:
whzy 2026-06-26 21:09:12 +08:00
parent 73da46c143
commit b67bd33fb7
16 changed files with 1597 additions and 156 deletions

View File

@ -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

View File

@ -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

View File

@ -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.
```

View File

@ -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:

View File

@ -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
```

View File

@ -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 |

View File

@ -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.

View File

@ -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。
i18nfeishu 命令 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。
```

View File

@ -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":

View File

@ -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 {

View File

@ -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",

View File

@ -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
}

View File

@ -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)
}

View File

@ -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" {

181
shortcuts/feishu/l10n.go Normal file
View File

@ -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"},
}

View File

@ -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
}