forked from Gitlink/gitlink-cli
feat(feishu): validate real exports and zh-CN output
This commit is contained in:
parent
73da46c143
commit
b67bd33fb7
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
```
|
||||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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
|
||||
```
|
||||
|
|
@ -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 |
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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。
|
||||
```
|
||||
|
|
@ -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":
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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" {
|
||||
|
|
|
|||
|
|
@ -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"},
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue