feat(feishu): add Open Platform readiness diagnostics
This commit is contained in:
parent
278b496118
commit
d7812df1af
|
|
@ -10,6 +10,7 @@ $env:FEISHU_APP_SECRET=""
|
|||
$env:FEISHU_WIKI_URL=""
|
||||
$env:FEISHU_WIKI_NODE_TOKEN=""
|
||||
$env:FEISHU_FOLDER_TOKEN=""
|
||||
$env:FEISHU_DOCUMENT_ID=""
|
||||
|
||||
# Feishu Base / Bitable
|
||||
$env:FEISHU_BASE_APP_TOKEN=""
|
||||
|
|
|
|||
|
|
@ -582,6 +582,17 @@ gitlink-cli feishu +owner-digest --from-workflow-json report.zh-CN.json --lang z
|
|||
gitlink-cli feishu +contributor-digest --from-workflow-json report.zh-CN.json --lang zh-CN --send --format table
|
||||
```
|
||||
|
||||
#### 配置诊断层:先检查再写入
|
||||
|
||||
诊断命令用于降低飞书开放平台配置成本。默认只检查本地变量和目标配置;传 `--remote` 后会调用飞书只读/检查接口,例如获取 `tenant_access_token`、解析 Wiki node、搜索 Bitable sentinel `unique_key`。这些命令不会创建文档、不会写入多维表格、不会创建任务,也不会修改 GitLink。
|
||||
|
||||
```bash
|
||||
gitlink-cli feishu +app-check --format table
|
||||
gitlink-cli feishu +doc-check --remote --format table
|
||||
gitlink-cli feishu +bitable-check --tables reports,issues,prs,tasks --remote --format table
|
||||
gitlink-cli feishu +task-check --remote --format table
|
||||
```
|
||||
|
||||
#### 实验层:飞书开放平台写入
|
||||
|
||||
实验层使用飞书开放平台自建应用。当前已在测试企业中验证 DocX 追加、多维表格写入和飞书任务创建,但这部分不是零配置稳定能力。真实写入仍然必须显式传 `--send`,并且要求自建应用有对应 API scope 和目标资源权限。
|
||||
|
|
|
|||
|
|
@ -75,6 +75,52 @@ Unit and mock tests are implemented.
|
|||
Real custom bot sending can be tested when FEISHU_WEBHOOK_URL exists.
|
||||
```
|
||||
|
||||
## Layer 1.5: Configuration Diagnostics
|
||||
|
||||
Status: stable read/check surface.
|
||||
|
||||
Purpose:
|
||||
|
||||
```text
|
||||
Help users discover missing env vars, invalid tokens, missing table IDs, missing
|
||||
unique_key fields, and next-stage Task limitations before running --send writes.
|
||||
```
|
||||
|
||||
Implemented commands:
|
||||
|
||||
```text
|
||||
gitlink-cli feishu +app-check
|
||||
gitlink-cli feishu +doc-check
|
||||
gitlink-cli feishu +bitable-check
|
||||
gitlink-cli feishu +task-check
|
||||
```
|
||||
|
||||
Default behavior:
|
||||
|
||||
```text
|
||||
Local checks only. No Feishu OpenAPI call is made unless --remote is explicit.
|
||||
```
|
||||
|
||||
Remote behavior:
|
||||
|
||||
```text
|
||||
--remote acquires tenant_access_token.
|
||||
+doc-check --remote may resolve a Wiki node.
|
||||
+bitable-check --remote searches a sentinel unique_key to verify table access
|
||||
and the unique_key field.
|
||||
+task-check --remote verifies app credentials only; it does not create tasks.
|
||||
```
|
||||
|
||||
What it cannot do:
|
||||
|
||||
```text
|
||||
Create Feishu resources.
|
||||
Modify Feishu resources.
|
||||
Modify GitLink resources.
|
||||
Guarantee DocX edit permission without an actual append.
|
||||
Guarantee Feishu Task project/section placement.
|
||||
```
|
||||
|
||||
## Layer 2: Experimental Open Platform Validation
|
||||
|
||||
Status: experimental validation surface.
|
||||
|
|
@ -120,6 +166,10 @@ FEISHU_TASK_SECTION_ID optional
|
|||
Implemented commands:
|
||||
|
||||
```text
|
||||
gitlink-cli feishu +app-check
|
||||
gitlink-cli feishu +doc-check
|
||||
gitlink-cli feishu +bitable-check
|
||||
gitlink-cli feishu +task-check
|
||||
gitlink-cli feishu +doc-export
|
||||
gitlink-cli feishu +bitable-sync
|
||||
gitlink-cli feishu +task-create
|
||||
|
|
|
|||
|
|
@ -42,8 +42,8 @@ $env:FEISHU_WEBHOOK_SECRET="REDACTED"
|
|||
|
||||
| Name | Purpose | Required | Used by | Sensitive | How to obtain |
|
||||
| --- | --- | --- | --- | --- | --- |
|
||||
| `FEISHU_APP_ID` | Self-built app ID | Required for Open Platform `--send` | `+doc-export`, `+bitable-sync`, `+task-create` | Yes | Feishu Open Platform app page |
|
||||
| `FEISHU_APP_SECRET` | Self-built app secret | Required for Open Platform `--send` | same as above | Yes | Feishu Open Platform app credentials |
|
||||
| `FEISHU_APP_ID` | Self-built app ID | Required for Open Platform `--send` or diagnostic `--remote` | `+app-check`, `+doc-check`, `+bitable-check`, `+task-check`, `+doc-export`, `+bitable-sync`, `+task-create` | Yes | Feishu Open Platform app page |
|
||||
| `FEISHU_APP_SECRET` | Self-built app secret | Required for Open Platform `--send` or diagnostic `--remote` | same as above | Yes | Feishu Open Platform app credentials |
|
||||
|
||||
Example:
|
||||
|
||||
|
|
@ -56,10 +56,10 @@ $env:FEISHU_APP_SECRET="REDACTED"
|
|||
|
||||
| Name | Purpose | Required | Used by | Sensitive | How to obtain |
|
||||
| --- | --- | --- | --- | --- | --- |
|
||||
| `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 |
|
||||
| `FEISHU_WIKI_URL` | Existing Wiki page URL | Optional target | `+doc-check`, `+doc-export` | Can expose workspace/resource ID | Copy from Feishu Wiki |
|
||||
| `FEISHU_WIKI_NODE_TOKEN` | Existing Wiki node token | Optional target | `+doc-check`, `+doc-export` | Yes | Parsed from Wiki URL or API |
|
||||
| `FEISHU_FOLDER_TOKEN` | Folder token for creating a new DocX | Optional target | `+doc-check`, `+doc-export` | Yes | Feishu Drive folder URL / Open Platform docs |
|
||||
| `FEISHU_DOCUMENT_ID` | Existing DocX document ID for append | Optional target | `+doc-check`, `+doc-export` | Yes | Existing Feishu DocX URL or Open Platform docs |
|
||||
|
||||
Legacy compatibility:
|
||||
|
||||
|
|
@ -87,12 +87,12 @@ go run . feishu +notify --from-workflow-json .local\report.zh-CN.json --lang zh-
|
|||
|
||||
| Name | Purpose | Required | Used by | Sensitive | How to obtain |
|
||||
| --- | --- | --- | --- | --- | --- |
|
||||
| `FEISHU_BASE_APP_TOKEN` | Base app token | Required for `+bitable-sync --send` | `+bitable-sync` | Yes | Feishu Base URL / Open Platform docs |
|
||||
| `FEISHU_REPORT_TABLE_ID` | Reports table ID | Required when syncing `reports` | `+bitable-sync` | Yes | Base table settings / API |
|
||||
| `FEISHU_ISSUE_TABLE_ID` | Issues table ID | Required when syncing `issues` | `+bitable-sync` | Yes | Base table settings / API |
|
||||
| `FEISHU_PR_TABLE_ID` | Pull request table ID | Required when syncing `prs` | `+bitable-sync` | Yes | Base table settings / API |
|
||||
| `FEISHU_CONTRIBUTOR_TABLE_ID` | Contributors table ID | Optional | `+bitable-sync` | Yes | Base table settings / API |
|
||||
| `FEISHU_TASK_TABLE_ID` | Task-candidate table ID | Optional | `+bitable-sync` | Yes | Base table settings / API |
|
||||
| `FEISHU_BASE_APP_TOKEN` | Base app token | Required for `+bitable-check` and `+bitable-sync --send` | `+bitable-check`, `+bitable-sync` | Yes | Feishu Base URL / Open Platform docs |
|
||||
| `FEISHU_REPORT_TABLE_ID` | Reports table ID | Required when checking or syncing `reports` | `+bitable-check`, `+bitable-sync` | Yes | Base table settings / API |
|
||||
| `FEISHU_ISSUE_TABLE_ID` | Issues table ID | Required when checking or syncing `issues` | `+bitable-check`, `+bitable-sync` | Yes | Base table settings / API |
|
||||
| `FEISHU_PR_TABLE_ID` | Pull request table ID | Required when checking or syncing `prs` | `+bitable-check`, `+bitable-sync` | Yes | Base table settings / API |
|
||||
| `FEISHU_CONTRIBUTOR_TABLE_ID` | Contributors table ID | Optional unless selected | `+bitable-check`, `+bitable-sync` | Yes | Base table settings / API |
|
||||
| `FEISHU_TASK_TABLE_ID` | Task-candidate table ID | Optional unless selected | `+bitable-check`, `+bitable-sync` | Yes | Base table settings / API |
|
||||
|
||||
Example:
|
||||
|
||||
|
|
@ -109,8 +109,8 @@ $env:FEISHU_TASK_TABLE_ID="REDACTED"
|
|||
|
||||
| Name | Purpose | Required | Used by | Sensitive | How to obtain |
|
||||
| --- | --- | --- | --- | --- | --- |
|
||||
| `FEISHU_TASK_PROJECT_ID` | Optional task project target | Optional | `+task-create` | Yes | Feishu Task project settings / API |
|
||||
| `FEISHU_TASK_SECTION_ID` | Optional task section target | Optional | `+task-create` | Yes | Feishu Task section settings / API |
|
||||
| `FEISHU_TASK_PROJECT_ID` | Optional task project target | Optional | `+task-check`, `+task-create` | Yes | Feishu Task project settings / API |
|
||||
| `FEISHU_TASK_SECTION_ID` | Optional task section target | Optional | `+task-check`, `+task-create` | Yes | Feishu Task section settings / API |
|
||||
|
||||
Current limitation:
|
||||
|
||||
|
|
|
|||
|
|
@ -56,11 +56,11 @@ https://www.feishu.cn/feishu-cli
|
|||
| --- | --- | --- | --- | --- | --- |
|
||||
| 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 |
|
||||
| Tenant token | `POST /auth/v3/tenant_access_token/internal` | `+app-check --remote`, `+doc-check --remote`, `+bitable-check --remote`, `+task-check --remote`, `+doc-export`, `+bitable-sync`, `+task-create` | Implemented | Tenant token | No token cache yet |
|
||||
| Wiki node resolution | `GET /wiki/v2/spaces/get_node?token=...` | `+doc-check --remote`, `+doc-export` | Implemented | 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 search | `POST /bitable/v1/apps/{app_token}/tables/{table_id}/records/search` | `+bitable-check --remote`, `+bitable-sync` | Implemented | Existing Base table | `+bitable-check` searches a sentinel key without writing |
|
||||
| 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 |
|
||||
|
|
@ -137,6 +137,10 @@ Current commands:
|
|||
+doc-export
|
||||
+bitable-sync
|
||||
+task-create
|
||||
+app-check
|
||||
+doc-check
|
||||
+bitable-check
|
||||
+task-check
|
||||
```
|
||||
|
||||
Inputs:
|
||||
|
|
@ -157,7 +161,8 @@ Does not print the raw token.
|
|||
Next hardening:
|
||||
|
||||
```text
|
||||
Add +app-check.
|
||||
`+app-check` now exists. Add richer scope-name hints once official scope names
|
||||
are mapped in the code.
|
||||
Cache token in memory during one command execution only.
|
||||
Add scope diagnostics where official scope names are confirmed.
|
||||
```
|
||||
|
|
@ -220,7 +225,8 @@ must be handled by the owner/admin outside the CLI.
|
|||
Next hardening:
|
||||
|
||||
```text
|
||||
Add +app-check diagnostics for DocX/Wiki scopes.
|
||||
`+doc-check` now validates DocX/Wiki target configuration and can resolve Wiki
|
||||
nodes with --remote.
|
||||
Add clearer output for target type: wiki node, existing doc, folder creation.
|
||||
Add optional markdown-only export for manual paste into Feishu Docs.
|
||||
```
|
||||
|
|
@ -240,6 +246,7 @@ Current commands:
|
|||
```text
|
||||
+bitable-schema
|
||||
+bitable-records
|
||||
+bitable-check
|
||||
+bitable-sync
|
||||
```
|
||||
|
||||
|
|
@ -272,6 +279,9 @@ Current behavior:
|
|||
```text
|
||||
+bitable-schema outputs a dry-run schema.
|
||||
+bitable-records outputs summary-oriented local records.
|
||||
+bitable-check validates configured table IDs and expected fields.
|
||||
+bitable-check --remote searches a sentinel unique_key to verify table access
|
||||
and the unique_key field without writing 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.
|
||||
|
|
@ -317,7 +327,8 @@ Current records are summary buckets, not full row-level PR/Issue/CI records.
|
|||
Next hardening:
|
||||
|
||||
```text
|
||||
Add table/field validation before writes.
|
||||
`+bitable-check` now provides pre-write table ID and unique_key search
|
||||
diagnostics. Field type validation still depends on richer Feishu field metadata.
|
||||
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.
|
||||
|
|
@ -335,6 +346,7 @@ Current commands:
|
|||
|
||||
```text
|
||||
+task-preview
|
||||
+task-check
|
||||
+task-create
|
||||
```
|
||||
|
||||
|
|
@ -352,6 +364,8 @@ Current behavior:
|
|||
|
||||
```text
|
||||
+task-preview generates local task candidates.
|
||||
+task-check validates app credentials and documents current project/section and
|
||||
dedupe limitations without creating tasks.
|
||||
+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.
|
||||
|
|
@ -386,7 +400,9 @@ 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.
|
||||
`+task-check --remote` validates app credentials without creating tasks. Add
|
||||
scope diagnostics and project/section request mapping after the official fields
|
||||
are confirmed.
|
||||
```
|
||||
|
||||
## i18n Validation
|
||||
|
|
@ -589,6 +605,7 @@ GitLink real data validation:
|
|||
[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] Read/check diagnostics implemented for app, DocX/Wiki, Bitable, and Task setup.
|
||||
[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.
|
||||
|
|
|
|||
|
|
@ -25,6 +25,7 @@ Real DocX append: passed through self-built app OpenAPI.
|
|||
Real Bitable sync: passed after target table fields were created.
|
||||
Split Bitable sync: passed with five separate tables.
|
||||
Real Task create: passed; project/section placement remains unmapped.
|
||||
Read/check diagnostics: passed for app, DocX target, Bitable tables, and Task credentials.
|
||||
GitLink write operations: not implemented and not tested.
|
||||
Test enterprise permissions: intentionally broad for validation; production should use minimum scopes.
|
||||
```
|
||||
|
|
@ -35,7 +36,7 @@ Test enterprise permissions: intentionally broad for validation; production shou
|
|||
| --- | --- | --- | --- |
|
||||
| Custom bot webhook | Complete and real-tested | `shortcuts/feishu/client.go`, `sign.go`, `card.go` | Image evidence deferred |
|
||||
| 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` |
|
||||
| tenant_access_token | Complete and real-tested | `OpenAPIClient.TenantAccessToken`, `+app-check --remote` | Add richer scope hints later |
|
||||
| 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 |
|
||||
|
|
@ -57,6 +58,10 @@ Test enterprise permissions: intentionally broad for validation; production shou
|
|||
| `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 +app-check` | Stable diagnostics | Implemented | No writes; optional read/check with `--remote` | App credentials for remote token check |
|
||||
| `feishu +doc-check` | Stable diagnostics | Implemented | No writes; optional Wiki read with `--remote` | App credentials and DocX/Wiki target |
|
||||
| `feishu +bitable-check` | Stable diagnostics | Implemented | No writes; optional Bitable search with `--remote` | Base app token, table IDs, `unique_key` |
|
||||
| `feishu +task-check` | Stable diagnostics | Implemented | No writes; optional token check with `--remote` | App credentials |
|
||||
| `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 |
|
||||
|
|
@ -75,6 +80,7 @@ Test enterprise permissions: intentionally broad for validation; production shou
|
|||
6. User-required environment variables are documented.
|
||||
7. Resource-level permission requirements are documented.
|
||||
8. Task project/section limitation is explicitly called out.
|
||||
9. Feishu configuration diagnostics are available before running --send writes.
|
||||
```
|
||||
|
||||
## What Still Needs User Action
|
||||
|
|
@ -157,6 +163,10 @@ Executed on 2026-06-26 after the API inventory update:
|
|||
| `+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 |
|
||||
| `+app-check --remote` | Pass | Custom bot, app credentials, and tenant_access_token validated with redacted output |
|
||||
| `+doc-check --remote` | Pass | App credentials and DocX/folder targets checked; write permission not probed without appending |
|
||||
| `+bitable-check --remote` | Pass | Five split tables passed sentinel `unique_key` search without record writes |
|
||||
| `+task-check --remote` | Pass with warnings | Tenant token passed; project/section/dedupe remain next-stage boundaries |
|
||||
| `+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 |
|
||||
| Split-table `+bitable-sync --send` | Pass | Created 1 report, 5 issue, 2 PR, 1 contributor, and 7 task records across five separate Bitable tables |
|
||||
|
|
|
|||
|
|
@ -45,7 +45,30 @@ This file is ignored and must not be committed. A tracked empty example is avail
|
|||
|
||||
The checker prints only redacted values.
|
||||
|
||||
## 3. Configure GitLink Test Repository Manually If Needed
|
||||
## 3. Run Feishu CLI Diagnostics
|
||||
|
||||
Local diagnostics:
|
||||
|
||||
```bash
|
||||
gitlink-cli feishu +app-check --format table
|
||||
gitlink-cli feishu +doc-check --format table
|
||||
gitlink-cli feishu +bitable-check --tables reports,issues,prs,contributors,tasks --format table
|
||||
gitlink-cli feishu +task-check --format table
|
||||
```
|
||||
|
||||
Optional remote diagnostics:
|
||||
|
||||
```bash
|
||||
gitlink-cli feishu +app-check --remote --format table
|
||||
gitlink-cli feishu +doc-check --remote --format table
|
||||
gitlink-cli feishu +bitable-check --tables reports,issues,prs,contributors,tasks --remote --format table
|
||||
gitlink-cli feishu +task-check --remote --format table
|
||||
```
|
||||
|
||||
Remote diagnostics call only read/check endpoints. They do not create DocX
|
||||
blocks, Bitable records, Feishu tasks, or GitLink writes.
|
||||
|
||||
## 4. Configure GitLink Test Repository Manually If Needed
|
||||
|
||||
```powershell
|
||||
$env:GITLINK_OWNER="OWNER"
|
||||
|
|
@ -58,7 +81,7 @@ If the current workflow command cannot filter specific PR IDs, keep the PR IDs i
|
|||
$env:GITLINK_TEST_PR_IDS="1,2,3"
|
||||
```
|
||||
|
||||
## 4. Run Scripted Smoke Tests
|
||||
## 5. Run Scripted Smoke Tests
|
||||
|
||||
Preview only:
|
||||
|
||||
|
|
@ -94,7 +117,7 @@ reports/feishu-real-smoke-terminal.log
|
|||
|
||||
The terminal log is ignored and must not be committed after real runs.
|
||||
|
||||
## 5. Generate Workflow Report JSON Manually
|
||||
## 6. Generate Workflow Report JSON Manually
|
||||
|
||||
```bash
|
||||
gitlink-cli workflow +repo-report \
|
||||
|
|
@ -105,13 +128,13 @@ gitlink-cli workflow +repo-report \
|
|||
|
||||
Windows PowerShell redirection may produce UTF-16 with BOM. The Feishu workflow JSON reader supports UTF-8 and UTF-16 BOM inputs.
|
||||
|
||||
## 6. Preview Feishu Notify Card
|
||||
## 7. Preview Feishu Notify Card
|
||||
|
||||
```bash
|
||||
gitlink-cli feishu +notify --from-workflow-json report.json --format json
|
||||
```
|
||||
|
||||
## 7. Send Feishu Notify Card
|
||||
## 8. Send Feishu Notify Card
|
||||
|
||||
```bash
|
||||
gitlink-cli feishu +notify --from-workflow-json report.json --send --format table
|
||||
|
|
@ -124,50 +147,50 @@ FEISHU_WEBHOOK_URL
|
|||
FEISHU_WEBHOOK_SECRET optional
|
||||
```
|
||||
|
||||
## 8. Render Weekly Report
|
||||
## 9. Render Weekly Report
|
||||
|
||||
```bash
|
||||
gitlink-cli feishu +weekly-report --from-workflow-json report.json --format markdown
|
||||
```
|
||||
|
||||
## 9. Send Weekly Report
|
||||
## 10. Send Weekly Report
|
||||
|
||||
```bash
|
||||
gitlink-cli feishu +weekly-report --from-workflow-json report.json --send --format table
|
||||
```
|
||||
|
||||
## 10. Generate Owner Digest
|
||||
## 11. Generate Owner Digest
|
||||
|
||||
```bash
|
||||
gitlink-cli feishu +owner-digest --from-workflow-json report.json --format markdown
|
||||
```
|
||||
|
||||
## 11. Send Owner Digest
|
||||
## 12. Send Owner Digest
|
||||
|
||||
```bash
|
||||
gitlink-cli feishu +owner-digest --from-workflow-json report.json --send --format table
|
||||
```
|
||||
|
||||
## 12. Generate Contributor Digest
|
||||
## 13. Generate Contributor Digest
|
||||
|
||||
```bash
|
||||
gitlink-cli feishu +contributor-digest --from-workflow-json report.json --format markdown
|
||||
```
|
||||
|
||||
## 13. Send Contributor Digest
|
||||
## 14. Send Contributor Digest
|
||||
|
||||
```bash
|
||||
gitlink-cli feishu +contributor-digest --from-workflow-json report.json --send --format table
|
||||
```
|
||||
|
||||
## 14. Generate Bitable-Ready Records
|
||||
## 15. Generate Bitable-Ready Records
|
||||
|
||||
```bash
|
||||
gitlink-cli feishu +bitable-schema --tables reports,issues,prs,contributors,tasks --format markdown
|
||||
gitlink-cli feishu +bitable-records --from-workflow-json report.json --format json
|
||||
```
|
||||
|
||||
## 15. Preview Bitable Sync
|
||||
## 16. Preview Bitable Sync
|
||||
|
||||
```bash
|
||||
gitlink-cli feishu +bitable-sync \
|
||||
|
|
@ -176,7 +199,7 @@ gitlink-cli feishu +bitable-sync \
|
|||
--format table
|
||||
```
|
||||
|
||||
## 16. Execute Bitable Sync
|
||||
## 17. Execute Bitable Sync
|
||||
|
||||
```bash
|
||||
gitlink-cli feishu +bitable-sync \
|
||||
|
|
@ -199,7 +222,7 @@ FEISHU_CONTRIBUTOR_TABLE_ID optional
|
|||
FEISHU_TASK_TABLE_ID optional
|
||||
```
|
||||
|
||||
## 17. Preview DocX / Wiki Export
|
||||
## 18. Preview DocX / Wiki Export
|
||||
|
||||
```bash
|
||||
gitlink-cli feishu +doc-export \
|
||||
|
|
@ -208,7 +231,7 @@ gitlink-cli feishu +doc-export \
|
|||
--format markdown
|
||||
```
|
||||
|
||||
## 18. Execute DocX / Wiki Export
|
||||
## 19. Execute DocX / Wiki Export
|
||||
|
||||
```bash
|
||||
gitlink-cli feishu +doc-export \
|
||||
|
|
@ -218,13 +241,13 @@ gitlink-cli feishu +doc-export \
|
|||
--format table
|
||||
```
|
||||
|
||||
## 19. Preview Feishu Tasks
|
||||
## 20. Preview Feishu Tasks
|
||||
|
||||
```bash
|
||||
gitlink-cli feishu +task-preview --from-workflow-json report.json --format markdown
|
||||
```
|
||||
|
||||
## 20. Create Feishu Tasks
|
||||
## 21. Create Feishu Tasks
|
||||
|
||||
```bash
|
||||
gitlink-cli feishu +task-create --from-workflow-json report.json --send --format table
|
||||
|
|
@ -239,12 +262,12 @@ FEISHU_TASK_PROJECT_ID optional
|
|||
FEISHU_TASK_SECTION_ID optional
|
||||
```
|
||||
|
||||
## 21. Image Evidence
|
||||
## 22. Image Evidence
|
||||
|
||||
Image evidence is deferred for this round. Do not add screenshots or image files
|
||||
to the upload.
|
||||
|
||||
## 22. Run Go Tests
|
||||
## 23. Run Go Tests
|
||||
|
||||
```bash
|
||||
gofmt -w shortcuts/feishu
|
||||
|
|
@ -254,7 +277,7 @@ go test ./shortcuts
|
|||
go test ./...
|
||||
```
|
||||
|
||||
## 23. Capture Evidence
|
||||
## 24. Capture Evidence
|
||||
|
||||
Capture terminal logs and command output only.
|
||||
|
||||
|
|
|
|||
|
|
@ -11,6 +11,10 @@ GitLink write permission is `No` for every implemented command in this branch.
|
|||
| 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 |
|
||||
| App diagnostics | `feishu +app-check` | Stable diagnostics | No | Yes for `--remote` | No | No | No | No | No | Yes | unit, mock remote, and real remote passed | Remote mode only gets tenant_access_token; no writes |
|
||||
| Doc diagnostics | `feishu +doc-check` | Stable diagnostics | No | Yes for `--remote` | Yes for Wiki node read | No | No | No | No | Yes | local and real remote diagnostics passed | Does not prove edit permission without append |
|
||||
| Bitable diagnostics | `feishu +bitable-check` | Stable diagnostics | No | Yes for `--remote` | No | Yes for remote search | No | No | No | Yes | unit, mock remote, and five-table real remote passed | Checks table access and unique_key search; does not create fields |
|
||||
| Task diagnostics | `feishu +task-check` | Stable diagnostics | No | Yes for `--remote` | No | No | No; token check only | No | No | Yes | mock remote and real remote passed with expected warnings | Does not create tasks; project/section still next-stage |
|
||||
| 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, including zh-CN | Local candidates only |
|
||||
|
|
|
|||
|
|
@ -104,6 +104,10 @@ The workflow command does not currently filter the report by explicit PR IDs, so
|
|||
| `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 +app-check --remote` | pass | custom bot, app credentials, and tenant_access_token checked with redacted output |
|
||||
| `feishu +doc-check --remote` | pass | app credentials and configured DocX/folder targets checked; edit/create permission intentionally not checked without writing |
|
||||
| `feishu +bitable-check --remote` | pass | five split Bitable tables passed sentinel `unique_key` search without writing records |
|
||||
| `feishu +task-check --remote` | pass with warnings | app credentials and tenant_access_token checked; project/section/dedupe remain next-stage boundaries |
|
||||
| `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 |
|
||||
|
|
@ -200,6 +204,8 @@ module. It was not fixed in this smoke run to avoid unrelated locale churn.
|
|||
| `go test ./shortcuts/workflow` | pass |
|
||||
| `go test ./shortcuts` | pass |
|
||||
| `go test ./...` | pass |
|
||||
| `go build .` | pass |
|
||||
| `go vet ./...` | pass |
|
||||
| Raw secret scan over tracked/unignored candidate files | pass |
|
||||
|
||||
## Known Limitations
|
||||
|
|
@ -208,11 +214,12 @@ module. It was not fixed in this smoke run to avoid unrelated locale churn.
|
|||
1. Bitable sync requires existing Base/table/fields; CLI does not create tables or views.
|
||||
2. The first smoke used one test table for all record groups; a later smoke created split tables and proved every record group can write to its own table.
|
||||
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; this is a next-stage capability boundary.
|
||||
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. Image evidence is deferred and is not part of this upload.
|
||||
4. `+bitable-check --remote` can verify table access and unique_key search before writes, but it still does not create fields or validate every field type.
|
||||
5. Feishu task creation does not yet map project/section placement into the request body; this is a next-stage capability boundary.
|
||||
6. Feishu-side task dedupe/search is not implemented; avoid repeated real task-create runs unless duplicates are acceptable.
|
||||
7. No Feishu callback server is implemented.
|
||||
8. No GitLink write operation is implemented.
|
||||
9. Image evidence is deferred and is not part of this upload.
|
||||
```
|
||||
|
||||
## Image Evidence
|
||||
|
|
|
|||
|
|
@ -68,7 +68,7 @@ if ($Layer -eq "stable" -or $Layer -eq "all") {
|
|||
|
||||
if ($Layer -eq "open-platform" -or $Layer -eq "all") {
|
||||
$allMissing += Test-Group -Title "Open Platform app" -Names @("FEISHU_APP_ID", "FEISHU_APP_SECRET")
|
||||
$allMissing += Test-Group -Title "DocX / Wiki" -Names @("FEISHU_WIKI_URL", "FEISHU_WIKI_NODE_TOKEN", "FEISHU_FOLDER_TOKEN") -Optional @("FEISHU_WIKI_URL", "FEISHU_WIKI_NODE_TOKEN", "FEISHU_FOLDER_TOKEN")
|
||||
$allMissing += Test-Group -Title "DocX / Wiki" -Names @("FEISHU_WIKI_URL", "FEISHU_WIKI_NODE_TOKEN", "FEISHU_FOLDER_TOKEN", "FEISHU_DOCUMENT_ID") -Optional @("FEISHU_WIKI_URL", "FEISHU_WIKI_NODE_TOKEN", "FEISHU_FOLDER_TOKEN", "FEISHU_DOCUMENT_ID")
|
||||
$allMissing += Test-Group -Title "Base / Bitable" -Names @("FEISHU_BASE_APP_TOKEN", "FEISHU_REPORT_TABLE_ID", "FEISHU_ISSUE_TABLE_ID", "FEISHU_PR_TABLE_ID", "FEISHU_CONTRIBUTOR_TABLE_ID", "FEISHU_TASK_TABLE_ID") -Optional @("FEISHU_CONTRIBUTOR_TABLE_ID", "FEISHU_TASK_TABLE_ID")
|
||||
$allMissing += Test-Group -Title "Feishu Task" -Names @("FEISHU_TASK_PROJECT_ID", "FEISHU_TASK_SECTION_ID") -Optional @("FEISHU_TASK_PROJECT_ID", "FEISHU_TASK_SECTION_ID")
|
||||
}
|
||||
|
|
|
|||
|
|
@ -112,12 +112,13 @@ if (Missing @("FEISHU_APP_ID", "FEISHU_APP_SECRET")) {
|
|||
$env:FEISHU_APP_SECRET = Read-SecretValue "FEISHU_APP_SECRET" (Env "FEISHU_APP_SECRET") "Self-built app secret."
|
||||
}
|
||||
|
||||
if (Missing @("FEISHU_WIKI_URL", "FEISHU_WIKI_NODE_TOKEN", "FEISHU_FOLDER_TOKEN")) {
|
||||
if (Missing @("FEISHU_WIKI_URL", "FEISHU_WIKI_NODE_TOKEN", "FEISHU_FOLDER_TOKEN", "FEISHU_DOCUMENT_ID")) {
|
||||
Open-Url "https://www.feishu.cn/" "Feishu Docs / Wiki"
|
||||
Pause-User "Open your target Wiki/Doc page or folder and copy the URL/token. Press Enter when ready."
|
||||
$env:FEISHU_WIKI_URL = Read-OptionalValue "FEISHU_WIKI_URL" (Env "FEISHU_WIKI_URL") "Existing Wiki or Doc URL for +doc-export."
|
||||
$env:FEISHU_WIKI_NODE_TOKEN = Read-OptionalValue "FEISHU_WIKI_NODE_TOKEN" (Env "FEISHU_WIKI_NODE_TOKEN") "Optional. Usually parsed from FEISHU_WIKI_URL."
|
||||
$env:FEISHU_FOLDER_TOKEN = Read-OptionalValue "FEISHU_FOLDER_TOKEN" (Env "FEISHU_FOLDER_TOKEN") "Optional. Used when creating a new DocX in a folder."
|
||||
$env:FEISHU_DOCUMENT_ID = Read-OptionalValue "FEISHU_DOCUMENT_ID" (Env "FEISHU_DOCUMENT_ID") "Optional. Existing DocX document ID for append."
|
||||
}
|
||||
|
||||
if (Missing @("FEISHU_BASE_APP_TOKEN", "FEISHU_REPORT_TABLE_ID", "FEISHU_ISSUE_TABLE_ID", "FEISHU_PR_TABLE_ID")) {
|
||||
|
|
@ -165,6 +166,7 @@ $vars = @(
|
|||
"FEISHU_WIKI_URL",
|
||||
"FEISHU_WIKI_NODE_TOKEN",
|
||||
"FEISHU_FOLDER_TOKEN",
|
||||
"FEISHU_DOCUMENT_ID",
|
||||
"FEISHU_BASE_APP_TOKEN",
|
||||
"FEISHU_REPORT_TABLE_ID",
|
||||
"FEISHU_ISSUE_TABLE_ID",
|
||||
|
|
|
|||
|
|
@ -269,8 +269,12 @@ try {
|
|||
$helpCommands = @(
|
||||
"+owner-digest",
|
||||
"+contributor-digest",
|
||||
"+app-check",
|
||||
"+doc-check",
|
||||
"+bitable-check",
|
||||
"+bitable-sync",
|
||||
"+task-preview",
|
||||
"+task-check",
|
||||
"+task-create"
|
||||
)
|
||||
Invoke-Cmd "feishu help" @("go", "run", ".", "feishu", "--help") $true
|
||||
|
|
@ -306,7 +310,26 @@ try {
|
|||
}
|
||||
|
||||
if ($runOpenPlatform) {
|
||||
if (Has-Env @("FEISHU_APP_ID", "FEISHU_APP_SECRET") -and (Has-Env @("FEISHU_WIKI_URL") -or Has-Env @("FEISHU_WIKI_NODE_TOKEN") -or Has-Env @("FEISHU_FOLDER_TOKEN"))) {
|
||||
if (Has-Env @("FEISHU_APP_ID", "FEISHU_APP_SECRET")) {
|
||||
Invoke-Cmd "app-check remote" @("go", "run", ".", "feishu", "+app-check", "--remote", "--format", "table") $false
|
||||
Invoke-Cmd "task-check remote" @("go", "run", ".", "feishu", "+task-check", "--remote", "--format", "table") $false
|
||||
} else {
|
||||
Add-Skip "app/task diagnostics" "missing FEISHU_APP_ID/FEISHU_APP_SECRET"
|
||||
}
|
||||
|
||||
if (Has-Env @("FEISHU_APP_ID", "FEISHU_APP_SECRET") -and (Has-Env @("FEISHU_WIKI_URL") -or Has-Env @("FEISHU_WIKI_NODE_TOKEN") -or Has-Env @("FEISHU_FOLDER_TOKEN") -or Has-Env @("FEISHU_DOCUMENT_ID"))) {
|
||||
Invoke-Cmd "doc-check remote" @("go", "run", ".", "feishu", "+doc-check", "--remote", "--format", "table") $false
|
||||
} else {
|
||||
Add-Skip "doc diagnostics" "missing FEISHU_APP_ID/FEISHU_APP_SECRET or DocX/Wiki target"
|
||||
}
|
||||
|
||||
if (Has-Env @("FEISHU_APP_ID", "FEISHU_APP_SECRET", "FEISHU_BASE_APP_TOKEN", "FEISHU_REPORT_TABLE_ID", "FEISHU_ISSUE_TABLE_ID", "FEISHU_PR_TABLE_ID")) {
|
||||
Invoke-Cmd "bitable-check remote" @("go", "run", ".", "feishu", "+bitable-check", "--tables", "reports,issues,prs,contributors,tasks", "--remote", "--format", "table") $false
|
||||
} else {
|
||||
Add-Skip "bitable diagnostics" "missing Feishu app credentials, base app token, or required table IDs"
|
||||
}
|
||||
|
||||
if (Has-Env @("FEISHU_APP_ID", "FEISHU_APP_SECRET") -and (Has-Env @("FEISHU_WIKI_URL") -or Has-Env @("FEISHU_WIKI_NODE_TOKEN") -or Has-Env @("FEISHU_FOLDER_TOKEN") -or Has-Env @("FEISHU_DOCUMENT_ID"))) {
|
||||
Invoke-Cmd "doc-export send" @("go", "run", ".", "feishu", "+doc-export", "--from-workflow-json", $ReportJSON, "--send", "--format", "table") $false
|
||||
} else {
|
||||
Add-Skip "doc-export send" "missing FEISHU_APP_ID/FEISHU_APP_SECRET or DocX/Wiki target"
|
||||
|
|
|
|||
|
|
@ -0,0 +1,394 @@
|
|||
package feishu
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"strings"
|
||||
"text/tabwriter"
|
||||
"time"
|
||||
|
||||
"github.com/gitlink-org/gitlink-cli/shortcuts/common"
|
||||
)
|
||||
|
||||
type DiagnosticOutput struct {
|
||||
Mode string `json:"mode"`
|
||||
Remote bool `json:"remote"`
|
||||
Layer string `json:"layer"`
|
||||
Summary DiagnosticSummary `json:"summary"`
|
||||
Checks []DiagnosticCheck `json:"checks"`
|
||||
Warnings []string `json:"warnings,omitempty"`
|
||||
}
|
||||
|
||||
type DiagnosticSummary struct {
|
||||
Passed int `json:"passed"`
|
||||
Warned int `json:"warned"`
|
||||
Failed int `json:"failed"`
|
||||
Skipped int `json:"skipped"`
|
||||
}
|
||||
|
||||
type DiagnosticCheck struct {
|
||||
Name string `json:"name"`
|
||||
Status string `json:"status"`
|
||||
Required bool `json:"required"`
|
||||
Target string `json:"target,omitempty"`
|
||||
Value string `json:"value,omitempty"`
|
||||
Detail string `json:"detail,omitempty"`
|
||||
Hint string `json:"hint,omitempty"`
|
||||
}
|
||||
|
||||
func runFeishuAppCheck(ctx *common.RuntimeContext) error {
|
||||
output := DiagnosticOutput{
|
||||
Mode: "check",
|
||||
Remote: parseBool(ctx.Arg("remote")),
|
||||
Layer: "app",
|
||||
Warnings: []string{
|
||||
"Diagnostic commands never write Feishu resources or GitLink resources.",
|
||||
"Use --remote to call Feishu OpenAPI read/check endpoints.",
|
||||
},
|
||||
}
|
||||
webhookURL := firstNonEmpty(ctx.Arg("webhook-url"), os.Getenv("FEISHU_WEBHOOK_URL"))
|
||||
webhookSecret := firstNonEmpty(ctx.Arg("secret"), os.Getenv("FEISHU_WEBHOOK_SECRET"))
|
||||
appID := firstNonEmpty(ctx.Arg("app-id"), os.Getenv("FEISHU_APP_ID"))
|
||||
appSecret := firstNonEmpty(ctx.Arg("app-secret"), os.Getenv("FEISHU_APP_SECRET"))
|
||||
|
||||
if strings.TrimSpace(webhookURL) == "" {
|
||||
output.addCheck(warnCheck("custom bot webhook", "FEISHU_WEBHOOK_URL", "missing", "stable webhook send commands need FEISHU_WEBHOOK_URL"))
|
||||
} else if err := validateWebhookURL(webhookURL); err != nil {
|
||||
output.addCheck(failCheck("custom bot webhook", "FEISHU_WEBHOOK_URL", redactWebhookURL(webhookURL), err.Error(), "copy the full custom bot webhook URL from the Feishu group bot settings"))
|
||||
} else {
|
||||
output.addCheck(passCheck("custom bot webhook", "FEISHU_WEBHOOK_URL", redactWebhookURL(webhookURL), "configured"))
|
||||
}
|
||||
if strings.TrimSpace(webhookSecret) == "" {
|
||||
output.addCheck(warnCheck("custom bot signing secret", "FEISHU_WEBHOOK_SECRET", "missing", "only required when the custom bot enables signature verification"))
|
||||
} else {
|
||||
output.addCheck(passCheck("custom bot signing secret", "FEISHU_WEBHOOK_SECRET", redactToken(webhookSecret), "configured"))
|
||||
}
|
||||
output.addCheck(requiredSecretCheck("self-built app id", "FEISHU_APP_ID", appID, "required for DocX, Bitable, and Task OpenAPI validation"))
|
||||
output.addCheck(requiredSecretCheck("self-built app secret", "FEISHU_APP_SECRET", appSecret, "required for tenant_access_token"))
|
||||
if output.Remote && output.Summary.Failed == 0 {
|
||||
output.remoteTenantToken(appID, appSecret)
|
||||
} else if output.Remote {
|
||||
output.addCheck(skipCheck("tenant_access_token", "Feishu OpenAPI", "skipped because app credentials are incomplete"))
|
||||
}
|
||||
return finishDiagnostic(ctx, output)
|
||||
}
|
||||
|
||||
func runFeishuDocCheck(ctx *common.RuntimeContext) error {
|
||||
opts := DocExportOptions{
|
||||
AppID: firstNonEmpty(ctx.Arg("app-id"), os.Getenv("FEISHU_APP_ID")),
|
||||
AppSecret: firstNonEmpty(ctx.Arg("app-secret"), os.Getenv("FEISHU_APP_SECRET")),
|
||||
FolderToken: firstNonEmpty(ctx.Arg("folder-token"), os.Getenv("FEISHU_FOLDER_TOKEN"), os.Getenv("FEISHU_DOC_FOLDER_TOKEN")),
|
||||
DocumentID: firstNonEmpty(ctx.Arg("document-id"), os.Getenv("FEISHU_DOCUMENT_ID")),
|
||||
WikiURL: firstNonEmpty(ctx.Arg("wiki-url"), os.Getenv("FEISHU_WIKI_URL")),
|
||||
WikiNodeToken: firstNonEmpty(ctx.Arg("wiki-node-token"), os.Getenv("FEISHU_WIKI_NODE_TOKEN")),
|
||||
}
|
||||
if opts.WikiNodeToken == "" && opts.WikiURL != "" {
|
||||
opts.WikiNodeToken = wikiNodeTokenFromURL(opts.WikiURL)
|
||||
}
|
||||
if opts.DocumentID == "" && opts.WikiURL != "" {
|
||||
opts.DocumentID = docxTokenFromURL(opts.WikiURL)
|
||||
}
|
||||
output := DiagnosticOutput{
|
||||
Mode: "check",
|
||||
Remote: parseBool(ctx.Arg("remote")),
|
||||
Layer: "docx",
|
||||
Warnings: []string{
|
||||
"Doc check does not append blocks or create documents.",
|
||||
"Actual DocX/Wiki writes still require --send on +doc-export.",
|
||||
},
|
||||
}
|
||||
output.addCheck(requiredSecretCheck("self-built app id", "FEISHU_APP_ID", opts.AppID, "required for DocX/Wiki OpenAPI"))
|
||||
output.addCheck(requiredSecretCheck("self-built app secret", "FEISHU_APP_SECRET", opts.AppSecret, "required for tenant_access_token"))
|
||||
|
||||
targets := 0
|
||||
if opts.DocumentID != "" {
|
||||
targets++
|
||||
output.addCheck(passCheck("existing document target", "FEISHU_DOCUMENT_ID", redactToken(opts.DocumentID), "configured for append path"))
|
||||
}
|
||||
if opts.WikiNodeToken != "" {
|
||||
targets++
|
||||
output.addCheck(passCheck("wiki node target", "FEISHU_WIKI_NODE_TOKEN", redactToken(opts.WikiNodeToken), "configured or parsed from wiki URL"))
|
||||
}
|
||||
if opts.FolderToken != "" {
|
||||
targets++
|
||||
output.addCheck(passCheck("folder target", "FEISHU_FOLDER_TOKEN", redactToken(opts.FolderToken), "configured for create path"))
|
||||
}
|
||||
if opts.WikiURL != "" {
|
||||
output.addCheck(passCheck("wiki url", "FEISHU_WIKI_URL", redactResourceURL(opts.WikiURL), "configured"))
|
||||
}
|
||||
if targets == 0 {
|
||||
output.addCheck(failCheck("docx target", "DocX/Wiki", "missing", "no document, wiki, or folder target configured", "set FEISHU_DOCUMENT_ID, FEISHU_WIKI_URL, FEISHU_WIKI_NODE_TOKEN, or FEISHU_FOLDER_TOKEN"))
|
||||
}
|
||||
if output.Remote && output.Summary.Failed == 0 {
|
||||
token, ok := output.remoteTenantToken(opts.AppID, opts.AppSecret)
|
||||
if ok && opts.WikiNodeToken != "" {
|
||||
output.remoteWikiNode(token.Value, opts.WikiNodeToken)
|
||||
}
|
||||
if ok && opts.WikiNodeToken == "" {
|
||||
output.addCheck(skipCheck("wiki node read", "Feishu Wiki", "skipped because no wiki node token was configured"))
|
||||
}
|
||||
if ok && opts.DocumentID != "" {
|
||||
output.addCheck(skipCheck("document edit permission", "Feishu DocX", "not checked without writing blocks"))
|
||||
}
|
||||
if ok && opts.FolderToken != "" {
|
||||
output.addCheck(skipCheck("folder create permission", "Feishu Drive", "not checked without creating a document"))
|
||||
}
|
||||
} else if output.Remote {
|
||||
output.addCheck(skipCheck("remote docx check", "Feishu OpenAPI", "skipped because required config is incomplete"))
|
||||
}
|
||||
return finishDiagnostic(ctx, output)
|
||||
}
|
||||
|
||||
func runFeishuBitableCheck(ctx *common.RuntimeContext) error {
|
||||
opts, err := bitableSyncOptionsFromContext(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
output := DiagnosticOutput{
|
||||
Mode: "check",
|
||||
Remote: parseBool(ctx.Arg("remote")),
|
||||
Layer: "bitable",
|
||||
Warnings: []string{
|
||||
"Bitable check does not create, update, or delete records.",
|
||||
"Remote mode searches a sentinel unique_key to verify table access and the unique_key field.",
|
||||
},
|
||||
}
|
||||
output.addCheck(requiredSecretCheck("self-built app id", "FEISHU_APP_ID", opts.AppID, "required for Base/Bitable OpenAPI"))
|
||||
output.addCheck(requiredSecretCheck("self-built app secret", "FEISHU_APP_SECRET", opts.AppSecret, "required for tenant_access_token"))
|
||||
output.addCheck(requiredSecretCheck("base app token", "FEISHU_BASE_APP_TOKEN", opts.BaseAppToken, "required for Bitable tables"))
|
||||
|
||||
for _, table := range opts.Tables {
|
||||
envName := tableEnvName(table)
|
||||
tableID := opts.TableIDs[table]
|
||||
output.addCheck(requiredSecretCheck(table+" table id", envName, tableID, "required for "+table+" sync"))
|
||||
fields := schemaForTable(table).Fields
|
||||
output.addCheck(passCheck(table+" expected fields", table, strings.Join(fieldNames(fields), ","), "schema expected by +bitable-sync"))
|
||||
}
|
||||
if output.Remote && output.Summary.Failed == 0 {
|
||||
token, ok := output.remoteTenantToken(opts.AppID, opts.AppSecret)
|
||||
if ok {
|
||||
client := NewOpenAPIClient(http.DefaultClient)
|
||||
checkCtx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||
defer cancel()
|
||||
for _, table := range opts.Tables {
|
||||
_, err := client.SearchBitableRecord(checkCtx, token.Value, opts.BaseAppToken, opts.TableIDs[table], "__gitlink_cli_check__")
|
||||
if err != nil {
|
||||
output.addCheck(failCheck("remote bitable table search", table, redactToken(opts.TableIDs[table]), sanitizeDiagnostic(diagnoseOpenAPIError(err, "bitable", table), opts.AppID, opts.AppSecret, opts.BaseAppToken, opts.TableIDs[table]), "grant Base scopes, share the Base with the app, and ensure unique_key exists"))
|
||||
continue
|
||||
}
|
||||
output.addCheck(passCheck("remote bitable table search", table, redactToken(opts.TableIDs[table]), "table accessible and unique_key search completed"))
|
||||
}
|
||||
}
|
||||
} else if output.Remote {
|
||||
output.addCheck(skipCheck("remote bitable check", "Feishu Base", "skipped because required config is incomplete"))
|
||||
}
|
||||
return finishDiagnostic(ctx, output)
|
||||
}
|
||||
|
||||
func runFeishuTaskCheck(ctx *common.RuntimeContext) error {
|
||||
opts, err := taskCreateOptionsFromContext(&common.RuntimeContext{Args: map[string]string{
|
||||
"app-id": ctx.Arg("app-id"),
|
||||
"app-secret": ctx.Arg("app-secret"),
|
||||
"task-project-id": ctx.Arg("task-project-id"),
|
||||
"task-section-id": ctx.Arg("task-section-id"),
|
||||
}})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
output := DiagnosticOutput{
|
||||
Mode: "check",
|
||||
Remote: parseBool(ctx.Arg("remote")),
|
||||
Layer: "task",
|
||||
Warnings: []string{
|
||||
"Task check does not create tasks.",
|
||||
"Current +task-create only sends basic summary and description.",
|
||||
},
|
||||
}
|
||||
output.addCheck(requiredSecretCheck("self-built app id", "FEISHU_APP_ID", opts.AppID, "required for Task OpenAPI"))
|
||||
output.addCheck(requiredSecretCheck("self-built app secret", "FEISHU_APP_SECRET", opts.AppSecret, "required for tenant_access_token"))
|
||||
if opts.TaskProjectID == "" {
|
||||
output.addCheck(warnCheck("task project id", "FEISHU_TASK_PROJECT_ID", "missing", "currently collected for future placement; not mapped into +task-create request body"))
|
||||
} else {
|
||||
output.addCheck(passCheck("task project id", "FEISHU_TASK_PROJECT_ID", redactToken(opts.TaskProjectID), "configured but not yet mapped into +task-create request body"))
|
||||
}
|
||||
if opts.TaskSectionID == "" {
|
||||
output.addCheck(warnCheck("task section id", "FEISHU_TASK_SECTION_ID", "missing", "currently collected for future placement; not mapped into +task-create request body"))
|
||||
} else {
|
||||
output.addCheck(passCheck("task section id", "FEISHU_TASK_SECTION_ID", redactToken(opts.TaskSectionID), "configured but not yet mapped into +task-create request body"))
|
||||
}
|
||||
output.addCheck(warnCheck("task dedupe", "Feishu Task", "not implemented", "dedupe is local unique_key only; no Feishu-side task search/linking"))
|
||||
if output.Remote && output.Summary.Failed == 0 {
|
||||
if _, ok := output.remoteTenantToken(opts.AppID, opts.AppSecret); ok {
|
||||
output.addCheck(skipCheck("remote task create permission", "Feishu Task", "not checked without creating a task"))
|
||||
} else {
|
||||
output.addCheck(skipCheck("remote task create permission", "Feishu Task", "not checked because tenant_access_token acquisition failed"))
|
||||
}
|
||||
} else if output.Remote {
|
||||
output.addCheck(skipCheck("remote task check", "Feishu Task", "skipped because app credentials are incomplete"))
|
||||
}
|
||||
return finishDiagnostic(ctx, output)
|
||||
}
|
||||
|
||||
func finishDiagnostic(ctx *common.RuntimeContext, output DiagnosticOutput) error {
|
||||
if err := renderDiagnosticOutput(os.Stdout, output, formatOrDefault(ctx, "table")); err != nil {
|
||||
return err
|
||||
}
|
||||
if output.Summary.Failed > 0 {
|
||||
return fmt.Errorf("Feishu %s check failed: %d failed check(s)", output.Layer, output.Summary.Failed)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (o *DiagnosticOutput) remoteTenantToken(appID, appSecret string) (TenantToken, bool) {
|
||||
client := NewOpenAPIClient(http.DefaultClient)
|
||||
checkCtx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||
defer cancel()
|
||||
token, err := client.TenantAccessToken(checkCtx, appID, appSecret)
|
||||
if err != nil {
|
||||
o.addCheck(failCheck("tenant_access_token", "Feishu OpenAPI", "failed", sanitizeDiagnostic(diagnoseOpenAPIError(err, "tenant token", "self-built app"), appID, appSecret), "verify app_id/app_secret and app availability"))
|
||||
return TenantToken{}, false
|
||||
}
|
||||
o.addCheck(passCheck("tenant_access_token", "Feishu OpenAPI", fmt.Sprintf("expire=%d", token.Expire), "acquired"))
|
||||
return token, true
|
||||
}
|
||||
|
||||
func (o *DiagnosticOutput) remoteWikiNode(token string, wikiNodeToken string) {
|
||||
client := NewOpenAPIClient(http.DefaultClient)
|
||||
checkCtx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||
defer cancel()
|
||||
node, err := client.GetWikiNode(checkCtx, token, wikiNodeToken)
|
||||
if err != nil {
|
||||
o.addCheck(failCheck("wiki node read", "Feishu Wiki", redactToken(wikiNodeToken), sanitizeDiagnostic(diagnoseOpenAPIError(err, "docx", "wiki node"), wikiNodeToken, token), "grant Wiki/DocX scopes and make the target Wiki node visible to the app"))
|
||||
return
|
||||
}
|
||||
detail := "obj_type=" + firstNonEmpty(node.ObjType, "unknown")
|
||||
if node.Title != "" {
|
||||
detail += "; title=" + node.Title
|
||||
}
|
||||
o.addCheck(passCheck("wiki node read", "Feishu Wiki", redactToken(wikiNodeToken), detail))
|
||||
}
|
||||
|
||||
func (o *DiagnosticOutput) addCheck(check DiagnosticCheck) {
|
||||
o.Checks = append(o.Checks, check)
|
||||
switch check.Status {
|
||||
case "pass":
|
||||
o.Summary.Passed++
|
||||
case "warn":
|
||||
o.Summary.Warned++
|
||||
case "fail":
|
||||
o.Summary.Failed++
|
||||
case "skip":
|
||||
o.Summary.Skipped++
|
||||
}
|
||||
}
|
||||
|
||||
func requiredSecretCheck(name, target, value, hint string) DiagnosticCheck {
|
||||
if strings.TrimSpace(value) == "" {
|
||||
return failCheck(name, target, "missing", "required value is not configured", hint)
|
||||
}
|
||||
return passCheck(name, target, redactToken(value), "configured")
|
||||
}
|
||||
|
||||
func passCheck(name, target, value, detail string) DiagnosticCheck {
|
||||
return DiagnosticCheck{Name: name, Status: "pass", Target: target, Value: value, Detail: detail}
|
||||
}
|
||||
|
||||
func warnCheck(name, target, value, hint string) DiagnosticCheck {
|
||||
return DiagnosticCheck{Name: name, Status: "warn", Target: target, Value: value, Hint: hint}
|
||||
}
|
||||
|
||||
func failCheck(name, target, value, detail, hint string) DiagnosticCheck {
|
||||
return DiagnosticCheck{Name: name, Status: "fail", Required: true, Target: target, Value: value, Detail: detail, Hint: hint}
|
||||
}
|
||||
|
||||
func skipCheck(name, target, detail string) DiagnosticCheck {
|
||||
return DiagnosticCheck{Name: name, Status: "skip", Target: target, Detail: detail}
|
||||
}
|
||||
|
||||
func renderDiagnosticOutput(w io.Writer, output DiagnosticOutput, format string) error {
|
||||
switch normalizeFormat(format) {
|
||||
case "markdown":
|
||||
return writeDiagnosticMarkdown(w, output)
|
||||
case "json":
|
||||
return writeJSON(w, output)
|
||||
default:
|
||||
return writeDiagnosticTable(w, output)
|
||||
}
|
||||
}
|
||||
|
||||
func writeDiagnosticTable(w io.Writer, output DiagnosticOutput) error {
|
||||
tw := tabwriter.NewWriter(w, 0, 0, 2, ' ', 0)
|
||||
if _, err := fmt.Fprintf(tw, "LAYER\tREMOTE\tPASS\tWARN\tFAIL\tSKIP\n%s\t%t\t%d\t%d\t%d\t%d\n\n", output.Layer, output.Remote, output.Summary.Passed, output.Summary.Warned, output.Summary.Failed, output.Summary.Skipped); err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := fmt.Fprintln(tw, "CHECK\tSTATUS\tTARGET\tVALUE\tDETAIL\tHINT"); err != nil {
|
||||
return err
|
||||
}
|
||||
for _, check := range output.Checks {
|
||||
if _, err := fmt.Fprintf(tw, "%s\t%s\t%s\t%s\t%s\t%s\n", check.Name, check.Status, check.Target, check.Value, oneLine(check.Detail), oneLine(check.Hint)); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return tw.Flush()
|
||||
}
|
||||
|
||||
func writeDiagnosticMarkdown(w io.Writer, output DiagnosticOutput) error {
|
||||
if _, err := fmt.Fprintf(w, "# Feishu %s Check\n\n", titleWord(output.Layer)); err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := fmt.Fprintf(w, "- Remote: `%t`\n- Passed: `%d`\n- Warned: `%d`\n- Failed: `%d`\n- Skipped: `%d`\n\n", output.Remote, output.Summary.Passed, output.Summary.Warned, output.Summary.Failed, output.Summary.Skipped); err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := fmt.Fprintln(w, "| Check | Status | Target | Value | Detail | Hint |\n| --- | --- | --- | --- | --- | --- |"); err != nil {
|
||||
return err
|
||||
}
|
||||
for _, check := range output.Checks {
|
||||
if _, err := fmt.Fprintf(w, "| %s | %s | %s | %s | %s | %s |\n", check.Name, check.Status, check.Target, check.Value, oneLine(check.Detail), oneLine(check.Hint)); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func tableEnvName(table string) string {
|
||||
switch table {
|
||||
case "reports":
|
||||
return "FEISHU_REPORT_TABLE_ID"
|
||||
case "issues":
|
||||
return "FEISHU_ISSUE_TABLE_ID"
|
||||
case "prs":
|
||||
return "FEISHU_PR_TABLE_ID"
|
||||
case "contributors":
|
||||
return "FEISHU_CONTRIBUTOR_TABLE_ID"
|
||||
case "tasks":
|
||||
return "FEISHU_TASK_TABLE_ID"
|
||||
default:
|
||||
return "FEISHU_TABLE_ID"
|
||||
}
|
||||
}
|
||||
|
||||
func fieldNames(fields []BitableField) []string {
|
||||
names := make([]string, 0, len(fields))
|
||||
for _, field := range fields {
|
||||
names = append(names, field.Name)
|
||||
}
|
||||
return names
|
||||
}
|
||||
|
||||
func sanitizeDiagnostic(message string, values ...string) string {
|
||||
for _, value := range values {
|
||||
value = strings.TrimSpace(value)
|
||||
if value == "" {
|
||||
continue
|
||||
}
|
||||
message = strings.ReplaceAll(message, value, redactToken(value))
|
||||
}
|
||||
return message
|
||||
}
|
||||
|
||||
func oneLine(value string) string {
|
||||
return strings.Join(strings.Fields(value), " ")
|
||||
}
|
||||
|
|
@ -26,6 +26,10 @@ func Shortcuts(translators ...*i18n.Translator) []*common.Shortcut {
|
|||
newWeeklyReportShortcut(),
|
||||
newOwnerDigestShortcut(),
|
||||
newContributorDigestShortcut(),
|
||||
newAppCheckShortcut(),
|
||||
newDocCheckShortcut(),
|
||||
newBitableCheckShortcut(),
|
||||
newTaskCheckShortcut(),
|
||||
newDocExportShortcut(),
|
||||
newBitableSchemaShortcut(),
|
||||
newBitableRecordsShortcut(),
|
||||
|
|
@ -35,6 +39,73 @@ func Shortcuts(translators ...*i18n.Translator) []*common.Shortcut {
|
|||
}
|
||||
}
|
||||
|
||||
func newAppCheckShortcut() *common.Shortcut {
|
||||
return &common.Shortcut{
|
||||
Name: "app-check",
|
||||
Description: "Check Feishu custom bot and self-built app configuration without writing resources",
|
||||
Flags: []common.Flag{
|
||||
{Name: "webhook-url", Usage: "Feishu custom bot webhook URL. Defaults to FEISHU_WEBHOOK_URL"},
|
||||
{Name: "secret", Usage: "Feishu custom bot signing secret. Defaults to FEISHU_WEBHOOK_SECRET"},
|
||||
{Name: "app-id", Usage: "Feishu self-built app ID. Defaults to FEISHU_APP_ID"},
|
||||
{Name: "app-secret", Usage: "Feishu self-built app secret. Defaults to FEISHU_APP_SECRET"},
|
||||
{Name: "remote", Usage: "Call Feishu OpenAPI read/check endpoints. No resources are written", Bool: true, Default: "false"},
|
||||
},
|
||||
Run: runAppCheck,
|
||||
}
|
||||
}
|
||||
|
||||
func newDocCheckShortcut() *common.Shortcut {
|
||||
return &common.Shortcut{
|
||||
Name: "doc-check",
|
||||
Description: "Check Feishu DocX/Wiki export configuration without writing documents",
|
||||
Flags: []common.Flag{
|
||||
{Name: "app-id", Usage: "Feishu self-built app ID. Defaults to FEISHU_APP_ID"},
|
||||
{Name: "app-secret", Usage: "Feishu self-built app secret. Defaults to FEISHU_APP_SECRET"},
|
||||
{Name: "folder-token", Usage: "Feishu folder token for creating a new DocX. Defaults to FEISHU_FOLDER_TOKEN"},
|
||||
{Name: "document-id", Usage: "Existing Feishu DocX document ID. Defaults to FEISHU_DOCUMENT_ID"},
|
||||
{Name: "wiki-url", Usage: "Existing Feishu Wiki URL. Defaults to FEISHU_WIKI_URL"},
|
||||
{Name: "wiki-node-token", Usage: "Existing Feishu Wiki node token. Defaults to FEISHU_WIKI_NODE_TOKEN"},
|
||||
{Name: "remote", Usage: "Call Feishu OpenAPI read/check endpoints. No resources are written", Bool: true, Default: "false"},
|
||||
},
|
||||
Run: runDocCheck,
|
||||
}
|
||||
}
|
||||
|
||||
func newBitableCheckShortcut() *common.Shortcut {
|
||||
return &common.Shortcut{
|
||||
Name: "bitable-check",
|
||||
Description: "Check Feishu Base/Bitable sync configuration and table readiness without writing records",
|
||||
Flags: []common.Flag{
|
||||
{Name: "tables", Usage: "Comma-separated tables: reports,issues,prs,contributors,tasks", Default: defaultTables},
|
||||
{Name: "app-id", Usage: "Feishu self-built app ID. Defaults to FEISHU_APP_ID"},
|
||||
{Name: "app-secret", Usage: "Feishu self-built app secret. Defaults to FEISHU_APP_SECRET"},
|
||||
{Name: "base-app-token", Usage: "Feishu Base app token. Defaults to FEISHU_BASE_APP_TOKEN"},
|
||||
{Name: "report-table-id", Usage: "Reports table ID. Defaults to FEISHU_REPORT_TABLE_ID"},
|
||||
{Name: "issue-table-id", Usage: "Issues table ID. Defaults to FEISHU_ISSUE_TABLE_ID"},
|
||||
{Name: "pr-table-id", Usage: "Pull requests table ID. Defaults to FEISHU_PR_TABLE_ID"},
|
||||
{Name: "contributor-table-id", Usage: "Contributors table ID. Defaults to FEISHU_CONTRIBUTOR_TABLE_ID"},
|
||||
{Name: "task-table-id", Usage: "Tasks table ID. Defaults to FEISHU_TASK_TABLE_ID"},
|
||||
{Name: "remote", Usage: "Call Feishu OpenAPI read/check endpoints. No resources are written", Bool: true, Default: "false"},
|
||||
},
|
||||
Run: runBitableCheck,
|
||||
}
|
||||
}
|
||||
|
||||
func newTaskCheckShortcut() *common.Shortcut {
|
||||
return &common.Shortcut{
|
||||
Name: "task-check",
|
||||
Description: "Check Feishu Task configuration without creating tasks",
|
||||
Flags: []common.Flag{
|
||||
{Name: "app-id", Usage: "Feishu self-built app ID. Defaults to FEISHU_APP_ID"},
|
||||
{Name: "app-secret", Usage: "Feishu self-built app secret. Defaults to FEISHU_APP_SECRET"},
|
||||
{Name: "task-project-id", Usage: "Feishu task project ID. Defaults to FEISHU_TASK_PROJECT_ID"},
|
||||
{Name: "task-section-id", Usage: "Feishu task section ID. Defaults to FEISHU_TASK_SECTION_ID"},
|
||||
{Name: "remote", Usage: "Call Feishu OpenAPI read/check endpoints. No resources are written", Bool: true, Default: "false"},
|
||||
},
|
||||
Run: runTaskCheck,
|
||||
}
|
||||
}
|
||||
|
||||
func newBotTestShortcut() *common.Shortcut {
|
||||
return &common.Shortcut{
|
||||
Name: "bot-test",
|
||||
|
|
@ -299,6 +370,22 @@ func runContributorDigest(ctx *common.RuntimeContext) error {
|
|||
return renderDigest(os.Stdout, digest, formatOrDefault(ctx, "markdown"), normalizeLang(ctx.Arg("lang")))
|
||||
}
|
||||
|
||||
func runAppCheck(ctx *common.RuntimeContext) error {
|
||||
return runFeishuAppCheck(ctx)
|
||||
}
|
||||
|
||||
func runDocCheck(ctx *common.RuntimeContext) error {
|
||||
return runFeishuDocCheck(ctx)
|
||||
}
|
||||
|
||||
func runBitableCheck(ctx *common.RuntimeContext) error {
|
||||
return runFeishuBitableCheck(ctx)
|
||||
}
|
||||
|
||||
func runTaskCheck(ctx *common.RuntimeContext) error {
|
||||
return runFeishuTaskCheck(ctx)
|
||||
}
|
||||
|
||||
func runDocExport(ctx *common.RuntimeContext) error {
|
||||
opts, err := docExportOptionsFromContext(ctx)
|
||||
if err != nil {
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@ func TestShortcutsExposeExpectedCommands(t *testing.T) {
|
|||
for _, shortcut := range Shortcuts() {
|
||||
got[shortcut.Name] = true
|
||||
}
|
||||
for _, name := range []string{"bot-test", "notify", "weekly-report", "owner-digest", "contributor-digest", "doc-export", "bitable-schema", "bitable-records", "bitable-sync", "task-preview", "task-create"} {
|
||||
for _, name := range []string{"bot-test", "notify", "weekly-report", "owner-digest", "contributor-digest", "app-check", "doc-check", "bitable-check", "task-check", "doc-export", "bitable-schema", "bitable-records", "bitable-sync", "task-preview", "task-create"} {
|
||||
if !got[name] {
|
||||
t.Fatalf("Shortcuts missing %s", name)
|
||||
}
|
||||
|
|
@ -316,6 +316,103 @@ func TestBitableSyncMockHTTP(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestAppCheckRemoteMockHTTP(t *testing.T) {
|
||||
var sawToken bool
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
if r.Method == http.MethodPost && r.URL.Path == "/auth/v3/tenant_access_token/internal" {
|
||||
sawToken = true
|
||||
_, _ = w.Write([]byte(`{"code":0,"msg":"success","tenant_access_token":"tenant-token","expire":7200}`))
|
||||
return
|
||||
}
|
||||
t.Fatalf("unexpected request: %s %s", r.Method, r.URL.Path)
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
oldBaseURL := openAPIBaseURL
|
||||
openAPIBaseURL = server.URL
|
||||
defer func() { openAPIBaseURL = oldBaseURL }()
|
||||
|
||||
ctx := &common.RuntimeContext{Args: map[string]string{
|
||||
"app-id": "cli_xxx",
|
||||
"app-secret": "secret",
|
||||
"webhook-url": "https://open.feishu.cn/open-apis/bot/v2/hook/test",
|
||||
"remote": "true",
|
||||
}}
|
||||
if err := runFeishuAppCheck(ctx); err != nil {
|
||||
t.Fatalf("runFeishuAppCheck returned error: %v", err)
|
||||
}
|
||||
if !sawToken {
|
||||
t.Fatal("expected tenant token request")
|
||||
}
|
||||
}
|
||||
|
||||
func TestBitableCheckRemoteUsesSearchOnly(t *testing.T) {
|
||||
var sawSearch bool
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
switch {
|
||||
case r.Method == http.MethodPost && r.URL.Path == "/auth/v3/tenant_access_token/internal":
|
||||
_, _ = w.Write([]byte(`{"code":0,"msg":"success","tenant_access_token":"tenant-token","expire":7200}`))
|
||||
case r.Method == http.MethodPost && r.URL.Path == "/bitable/v1/apps/base_token/tables/tbl_report/records/search":
|
||||
sawSearch = true
|
||||
_, _ = w.Write([]byte(`{"code":0,"msg":"success","data":{"items":[]}}`))
|
||||
default:
|
||||
t.Fatalf("unexpected request: %s %s", r.Method, r.URL.Path)
|
||||
}
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
oldBaseURL := openAPIBaseURL
|
||||
openAPIBaseURL = server.URL
|
||||
defer func() { openAPIBaseURL = oldBaseURL }()
|
||||
|
||||
ctx := &common.RuntimeContext{Args: map[string]string{
|
||||
"app-id": "cli_xxx",
|
||||
"app-secret": "secret",
|
||||
"base-app-token": "base_token",
|
||||
"tables": "reports",
|
||||
"report-table-id": "tbl_report",
|
||||
"remote": "true",
|
||||
}}
|
||||
if err := runFeishuBitableCheck(ctx); err != nil {
|
||||
t.Fatalf("runFeishuBitableCheck returned error: %v", err)
|
||||
}
|
||||
if !sawSearch {
|
||||
t.Fatal("expected bitable search request")
|
||||
}
|
||||
}
|
||||
|
||||
func TestTaskCheckRemoteDoesNotCreateTask(t *testing.T) {
|
||||
var requestCount int
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
requestCount++
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
if r.Method == http.MethodPost && r.URL.Path == "/auth/v3/tenant_access_token/internal" {
|
||||
_, _ = w.Write([]byte(`{"code":0,"msg":"success","tenant_access_token":"tenant-token","expire":7200}`))
|
||||
return
|
||||
}
|
||||
t.Fatalf("unexpected request: %s %s", r.Method, r.URL.Path)
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
oldBaseURL := openAPIBaseURL
|
||||
openAPIBaseURL = server.URL
|
||||
defer func() { openAPIBaseURL = oldBaseURL }()
|
||||
|
||||
ctx := &common.RuntimeContext{Args: map[string]string{
|
||||
"app-id": "cli_xxx",
|
||||
"app-secret": "secret",
|
||||
"remote": "true",
|
||||
}}
|
||||
if err := runFeishuTaskCheck(ctx); err != nil {
|
||||
t.Fatalf("runFeishuTaskCheck returned error: %v", err)
|
||||
}
|
||||
if requestCount != 1 {
|
||||
t.Fatalf("expected only tenant token request, got %d requests", requestCount)
|
||||
}
|
||||
}
|
||||
|
||||
func TestTaskCreateOptionsRejectSendDryRun(t *testing.T) {
|
||||
ctx := &common.RuntimeContext{Args: map[string]string{
|
||||
"send": "true",
|
||||
|
|
|
|||
Loading…
Reference in New Issue