Compare commits
3
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d5dbefb27e | ||
|
|
1c1abf3069 | ||
|
|
043881e0e3 |
+31
-16
@@ -4,23 +4,38 @@
|
|||||||
|
|
||||||
## High-Level Flow
|
## High-Level Flow
|
||||||
|
|
||||||
1. `internal/market` fetches the registry and server metadata.
|
1. `cmd` is the CLI entrypoint, invoking `internal/app` to build the root Cobra command tree.
|
||||||
2. `internal/discovery` resolves runtime server capabilities and caches results.
|
2. `internal/app` wires static utility commands (`auth`, `audit`, `schema`, `completion`) and dynamically loads product commands via `internal/plugin`.
|
||||||
3. `internal/ir` normalizes discovery output into one canonical tool catalog.
|
3. `internal/helpers` contains the main command handlers for all product surfaces (`dev`, `chat`, `calendar`, `contact`, `aitable`, etc.).
|
||||||
4. `internal/cli` and `internal/app` mount that catalog into the public Cobra command tree.
|
4. `internal/executor` and `internal/transport` execute MCP JSON-RPC calls; `internal/output` formats responses.
|
||||||
5. `internal/transport` executes MCP JSON-RPC calls and `internal/output` formats responses.
|
5. `internal/auth` manages login state, PAT tokens, and agent-code detection.
|
||||||
|
|
||||||
## Repository Structure
|
## Repository Structure
|
||||||
|
|
||||||
- `cmd`: CLI entrypoint
|
- `cmd`: CLI entrypoint
|
||||||
- `internal/app`: root command wiring and static utility commands
|
- `internal/app`: root command wiring, static utility commands, and plugin loading
|
||||||
- `internal/discovery`, `internal/market`, `internal/transport`: runtime discovery and execution
|
- `internal/helpers`: product command handlers (dev, chat, calendar, contact, etc.)
|
||||||
- `internal/ir`: canonical intermediate representation for discovered tools
|
- `internal/plugin`: plugin-based dynamic command loader
|
||||||
- `internal/generator`: docs, schema, and skill generation pipeline
|
- `internal/cli`: catalog types and endpoint loader (static endpoint mode)
|
||||||
- `internal/compat`, `internal/helpers`: legacy-compatible overlays and helper commands
|
- `internal/executor`: invocation dispatch and result handling
|
||||||
- `skills/`: bundled agent skills source and generated skill docs
|
- `internal/transport`: MCP HTTP client and request signing
|
||||||
- `test/`: CLI, compatibility, integration, contract, and script tests
|
- `internal/auth`: login, token management, agent-code detection, identity
|
||||||
|
- `internal/audit`: user operation audit log (JSONL, hash chain, forwarding)
|
||||||
## Public Repository Contract
|
- `internal/errors`: structured error model with categories and hints
|
||||||
|
- `internal/keychain`: OS keychain integration for credential storage
|
||||||
This repository ships source, docs, tests, packaging templates, and install scripts. Generated or release-only artifacts are produced by repository scripts and are not required to exist in a clean checkout unless explicitly committed as part of a release workflow.
|
- `internal/security`: endpoint allowlist and domain trust
|
||||||
|
- `internal/safety`: runtime safety checks (confirm prompts, dry-run guards)
|
||||||
|
- `internal/cobracmd`: shared Cobra command builders
|
||||||
|
- `internal/pat`: PAT (Personal Access Token) authorization flow
|
||||||
|
- `internal/output`: response formatting (json, table, raw, pretty)
|
||||||
|
- `internal/logging`: structured logging and argument sanitization
|
||||||
|
- `internal/tui`: terminal UI helpers
|
||||||
|
- `internal/recovery`: panic recovery and graceful degradation
|
||||||
|
- `pkg/configmeta`: environment variable registry and documentation
|
||||||
|
- `pkg/config`: configuration constants and paths
|
||||||
|
- `pkg/edition`: edition detection (oss vs enterprise)
|
||||||
|
- `pkg/mcptypes`: MCP protocol type definitions
|
||||||
|
- `skills/`: bundled agent skills (mono/ and multi/ layouts)
|
||||||
|
- `test/`: CLI, integration, contract, unit, and skill E2E tests
|
||||||
|
- `scripts/`: install scripts, policy checks, and CI helpers
|
||||||
|
- `envelope/`: pre-built discovery payloads for offline use
|
||||||
|
|||||||
@@ -0,0 +1,227 @@
|
|||||||
|
# 审计日志(Audit Log)
|
||||||
|
|
||||||
|
DWS 自动记录每次 MCP HTTP 调用的审计事件,用于合规追溯和安全审查。默认启用,无需额外配置。
|
||||||
|
|
||||||
|
## 功能特性
|
||||||
|
|
||||||
|
- **自动记录**:每次命令执行产生一条 JSONL 审计事件
|
||||||
|
- **按天轮转**:日志文件按日期分割(`audit-YYYYMMDD.jsonl`),默认留存 90 天
|
||||||
|
- **防篡改**:L1 哈希链(sha256),每条事件链接前一条的 hash,可验证完整性
|
||||||
|
- **远端转发**:支持 POST 到外部 SIEM 或审计平台
|
||||||
|
- **三级脱敏**:转发时可按 none/hashed/minimal 脱敏敏感字段
|
||||||
|
- **CLI 命令**:内置 `dws audit tail/export/verify` 查看和验证
|
||||||
|
|
||||||
|
## 环境变量
|
||||||
|
|
||||||
|
| 变量 | 默认值 | 说明 |
|
||||||
|
|------|--------|------|
|
||||||
|
| `DWS_AUDIT` | 启用 | 设 `0`/`false`/`off` 关闭审计 |
|
||||||
|
| `DWS_AUDIT_DIR` | `~/.dws/audit` | 审计日志目录 |
|
||||||
|
| `DWS_AUDIT_RETENTION_DAYS` | `90` | 日志留存天数 |
|
||||||
|
| `DWS_AUDIT_FORWARD_URL` | (空) | 远端转发 URL(POST JSON) |
|
||||||
|
| `DWS_AUDIT_FORWARD_TOKEN` | (空) | 远端转发 Bearer Token |
|
||||||
|
| `DWS_AUDIT_FORWARD_REDACT` | `none` | 转发脱敏级别:`none`/`hashed`/`minimal` |
|
||||||
|
|
||||||
|
## 审计事件格式
|
||||||
|
|
||||||
|
每条事件是一行 JSON,字段如下:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"ts": "2026-07-06T10:59:06+08:00",
|
||||||
|
"execution_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
|
||||||
|
"agent_id": "agent-xxx",
|
||||||
|
"actor": {
|
||||||
|
"user_id": "525018",
|
||||||
|
"name": "胡奕舟",
|
||||||
|
"corp_id": "ding8196cd9a2b2405da24f2f5cc6abecb85",
|
||||||
|
"corp_name": "钉钉"
|
||||||
|
},
|
||||||
|
"product": "calendar",
|
||||||
|
"command": "list_events",
|
||||||
|
"endpoint": "https://api.dingtalk.com/v1.0/calendar/users/xxx/calendars/primary/events",
|
||||||
|
"params_summary": "maxResults=20",
|
||||||
|
"result": "success",
|
||||||
|
"error_category": "",
|
||||||
|
"error_reason": "",
|
||||||
|
"duration_ms": 234,
|
||||||
|
"cli_version": "1.0.47",
|
||||||
|
"os": "darwin",
|
||||||
|
"arch": "arm64",
|
||||||
|
"prev_hash": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
|
||||||
|
"hash": "9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**字段说明**:
|
||||||
|
|
||||||
|
- `ts`:事件时间戳(RFC3339)
|
||||||
|
- `execution_id`:本次命令执行的唯一 ID
|
||||||
|
- `agent_id`:Agent 标识(若有)
|
||||||
|
- `actor`:执行者信息(从登录态获取)
|
||||||
|
- `product`:调用的产品(如 `calendar`、`chat`、`contact`)
|
||||||
|
- `command`:调用的命令(如 `list_events`、`send_message`)
|
||||||
|
- `endpoint`:实际请求的 API 端点(已脱敏)
|
||||||
|
- `params_summary`:参数摘要(已脱敏)
|
||||||
|
- `result`:`success` 或 `error`
|
||||||
|
- `error_category` / `error_reason`:错误分类和原因(成功时为空)
|
||||||
|
- `duration_ms`:命令执行耗时(毫秒)
|
||||||
|
- `prev_hash` / `hash`:哈希链字段,用于防篡改验证
|
||||||
|
|
||||||
|
## CLI 命令
|
||||||
|
|
||||||
|
### 查看最近日志
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 查看最近 20 条审计事件(默认)
|
||||||
|
dws audit tail
|
||||||
|
|
||||||
|
# 查看最近 50 条
|
||||||
|
dws audit tail -n 50
|
||||||
|
```
|
||||||
|
|
||||||
|
输出示例:
|
||||||
|
|
||||||
|
```
|
||||||
|
2026-07-06 10:59:06 calendar list_events user=525018 result=success 234ms
|
||||||
|
2026-07-06 10:58:42 chat send_message user=525018 result=success 156ms
|
||||||
|
2026-07-06 10:57:15 contact search user=525018 result=success 89ms
|
||||||
|
```
|
||||||
|
|
||||||
|
### 导出日志
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 导出最近 7 天的 JSONL
|
||||||
|
dws audit export --since 2026-06-29 --until 2026-07-06 --format jsonl > audit-7d.jsonl
|
||||||
|
|
||||||
|
# 导出为 CSV(方便 Excel 打开)
|
||||||
|
dws audit export --since 2026-07-01 --format csv > audit-july.csv
|
||||||
|
```
|
||||||
|
|
||||||
|
### 验证哈希链
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 验证当前最新日志文件的哈希链完整性
|
||||||
|
dws audit verify
|
||||||
|
|
||||||
|
# 验证指定文件
|
||||||
|
dws audit verify --file ~/.dws/audit/audit-20260706.jsonl
|
||||||
|
```
|
||||||
|
|
||||||
|
输出:
|
||||||
|
|
||||||
|
```
|
||||||
|
✓ 哈希链完整:234 条事件全部校验通过
|
||||||
|
```
|
||||||
|
|
||||||
|
或:
|
||||||
|
|
||||||
|
```
|
||||||
|
✗ 哈希链断裂:第 156 条事件的 hash 不匹配
|
||||||
|
```
|
||||||
|
|
||||||
|
## 哈希链防篡改
|
||||||
|
|
||||||
|
每条事件的 `hash` 字段由以下公式计算:
|
||||||
|
|
||||||
|
```
|
||||||
|
hash = sha256(prev_hash + event_json_without_hash_fields)
|
||||||
|
```
|
||||||
|
|
||||||
|
- 首条事件的 `prev_hash` 为空字符串
|
||||||
|
- 后续事件的 `prev_hash` = 前一条的 `hash`
|
||||||
|
- 任何对历史事件的篡改都会导致后续所有 hash 失效
|
||||||
|
|
||||||
|
**验证流程**:
|
||||||
|
|
||||||
|
1. 读取日志文件,逐行解析
|
||||||
|
2. 对每条事件,移除 `prev_hash` 和 `hash` 字段,重新序列化
|
||||||
|
3. 用前一条的 hash + 当前事件 JSON 计算新 hash
|
||||||
|
4. 对比计算结果与文件中记录的 hash
|
||||||
|
5. 全部匹配 = 完整;某条不匹配 = 被篡改
|
||||||
|
|
||||||
|
## 远端转发
|
||||||
|
|
||||||
|
设置 `DWS_AUDIT_FORWARD_URL` 后,每条审计事件会异步 POST 到指定端点:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
export DWS_AUDIT_FORWARD_URL=https://siem.example.com/ingest/audit
|
||||||
|
export DWS_AUDIT_FORWARD_TOKEN=your-bearer-token # 可选
|
||||||
|
```
|
||||||
|
|
||||||
|
**请求格式**:
|
||||||
|
|
||||||
|
```http
|
||||||
|
POST /ingest/audit HTTP/1.1
|
||||||
|
Host: siem.example.com
|
||||||
|
Authorization: Bearer your-bearer-token
|
||||||
|
Content-Type: application/json
|
||||||
|
|
||||||
|
{"ts":"2026-07-06T10:59:06+08:00","product":"calendar",...}
|
||||||
|
```
|
||||||
|
|
||||||
|
**超时与重试**:3 秒超时,失败不阻塞命令执行(best-effort),不自动重试。
|
||||||
|
|
||||||
|
## 脱敏分级
|
||||||
|
|
||||||
|
转发时可通过 `DWS_AUDIT_FORWARD_REDACT` 控制脱敏级别:
|
||||||
|
|
||||||
|
| 级别 | 行为 | 适用场景 |
|
||||||
|
|------|------|----------|
|
||||||
|
| `none`(默认) | 原样转发,不脱敏 | 内部审计平台 |
|
||||||
|
| `hashed` | actor.name 哈希化,params_summary 脱敏 | 跨部门共享 |
|
||||||
|
| `minimal` | 仅保留 ts/product/command/result/duration_ms,移除 actor/endpoint/params | 对外合规报告 |
|
||||||
|
|
||||||
|
**示例**:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 最小化脱敏(仅保留元数据)
|
||||||
|
export DWS_AUDIT_FORWARD_REDACT=minimal
|
||||||
|
export DWS_AUDIT_FORWARD_URL=https://compliance.example.com/audit
|
||||||
|
```
|
||||||
|
|
||||||
|
## 常见问题
|
||||||
|
|
||||||
|
### Q: 审计日志占多少磁盘?
|
||||||
|
|
||||||
|
典型场景(每天 100 条命令)约 50KB/天,90 天约 4.5MB。日志文件是 JSONL 纯文本,gzip 压缩后约 1/5。
|
||||||
|
|
||||||
|
### Q: 关闭审计会影响性能吗?
|
||||||
|
|
||||||
|
设置 `DWS_AUDIT=0` 后,审计模块不初始化,零开销。默认启用时,每条事件写入耗时 <1ms(异步磁盘 IO)。
|
||||||
|
|
||||||
|
### Q: 哈希链断了怎么办?
|
||||||
|
|
||||||
|
可能原因:
|
||||||
|
1. 手动编辑过日志文件
|
||||||
|
2. 磁盘损坏
|
||||||
|
3. 并发写入导致顺序错乱(罕见)
|
||||||
|
|
||||||
|
**处理**:
|
||||||
|
- 备份当前日志
|
||||||
|
- 用 `dws audit verify` 定位断裂位置
|
||||||
|
- 从断裂点之后的事件可继续验证(前缀已不可信)
|
||||||
|
|
||||||
|
### Q: 如何清理旧日志?
|
||||||
|
|
||||||
|
自动清理:`DWS_AUDIT_RETENTION_DAYS=90`(默认),超过 90 天的文件在下次启动时 best-effort 删除。
|
||||||
|
|
||||||
|
手动清理:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 删除 2026 年 6 月之前的日志
|
||||||
|
rm ~/.dws/audit/audit-202605*.jsonl
|
||||||
|
```
|
||||||
|
|
||||||
|
## 技术实现
|
||||||
|
|
||||||
|
- **核心包**:`internal/audit`
|
||||||
|
- **集成点**:`internal/app/runner.go` 的 `executeInvocation` 方法(defer 调用 `emitAudit`)
|
||||||
|
- **身份获取**:从 `auth.LoadTokenData` 读取当前登录用户
|
||||||
|
- **参数脱敏**:调用 `logging.SanitizeArguments`(与现有日志脱敏逻辑一致)
|
||||||
|
|
||||||
|
## 相关文档
|
||||||
|
|
||||||
|
- [环境变量参考](./reference.md)
|
||||||
|
- [架构概览](./architecture.md)
|
||||||
|
- [自动化与脚本](./automation.md)
|
||||||
+26
-25
@@ -13,40 +13,44 @@ repository root while preserving repo-local guidance for automation.
|
|||||||
## Project Snapshot
|
## Project Snapshot
|
||||||
|
|
||||||
- `dws` is a Go-based DingTalk Workspace CLI and MCP runtime bridge.
|
- `dws` is a Go-based DingTalk Workspace CLI and MCP runtime bridge.
|
||||||
- One internal Tool IR drives canonical CLI, schema, docs, skills, and snapshots.
|
- Product commands are loaded dynamically via `internal/plugin` from bundled descriptors.
|
||||||
- Compatibility and helper surfaces are overlays, not the canonical truth.
|
- Command handlers live in `internal/helpers`; runtime execution flows through `internal/executor` and `internal/transport`.
|
||||||
|
|
||||||
## Repository Map
|
## Repository Map
|
||||||
|
|
||||||
- `cmd`: public CLI entrypoint
|
- `cmd`: public CLI entrypoint
|
||||||
- `internal/app`: root command wiring and command tree mount points
|
- `internal/app`: root command wiring, static utility commands, plugin loading
|
||||||
- `internal/discovery`, `internal/market`, `internal/transport`: runtime discovery and MCP transport
|
- `internal/helpers`: product command handlers (dev, chat, calendar, contact, etc.)
|
||||||
- `internal/generator`: CLI/schema/docs/skills generation pipeline
|
- `internal/plugin`: plugin-based dynamic command loader
|
||||||
- `internal/compat`, `internal/helpers`: legacy-compatible aliases and helper commands
|
- `internal/cli`: catalog types and static endpoint loader
|
||||||
|
- `internal/executor`: invocation dispatch and result handling
|
||||||
|
- `internal/transport`: MCP HTTP client and request signing
|
||||||
|
- `internal/auth`: login, token management, agent-code detection
|
||||||
|
- `internal/audit`: user operation audit log
|
||||||
|
- `internal/errors`: structured error model with categories and hints
|
||||||
|
- `internal/keychain`: OS keychain integration for credential storage
|
||||||
|
- `internal/security`: endpoint allowlist and domain trust
|
||||||
|
- `internal/pat`: PAT (Personal Access Token) authorization flow
|
||||||
- `docs/`: public architecture and reference docs
|
- `docs/`: public architecture and reference docs
|
||||||
- `hack/`: developer-only helper commands not shipped as public binaries
|
|
||||||
- `scripts/`: build, test, lint, packaging, and policy checks
|
- `scripts/`: build, test, lint, packaging, and policy checks
|
||||||
- `test/`: integration, contract, compatibility, and script validation suites
|
- `test/`: CLI, integration, contract, unit, and skill E2E test suites
|
||||||
|
|
||||||
## Task Routing
|
## Task Routing
|
||||||
|
|
||||||
- Add or fix a command path: start from `internal/app` and the related module under `internal/*`
|
- Add or fix a command path: start from `internal/helpers` (handler implementations) or `internal/app` (command tree wiring)
|
||||||
- Discovery or protocol issues: inspect `internal/discovery`, `internal/market`, `internal/transport`
|
- Protocol or transport issues: inspect `internal/transport`
|
||||||
- Generated output drift: inspect `internal/generator` and run drift checks
|
- Auth or login issues: inspect `internal/auth`, `internal/pat`, `internal/keychain`
|
||||||
- Legacy behavior mismatch: inspect `internal/compat` and `test/cli_compat`
|
- Error message or category issues: inspect `internal/errors`
|
||||||
- Failure or degraded mode: inspect `internal/discovery`, `internal/errors`
|
- Audit log issues: inspect `internal/audit`
|
||||||
|
- Plugin loading or command surface: inspect `internal/plugin`
|
||||||
|
- Failure or degraded mode: inspect `internal/errors`, `internal/recovery`
|
||||||
|
|
||||||
## Generated Artifacts
|
## Policy Checks
|
||||||
|
|
||||||
Prefer editing source logic instead of generated files directly.
|
When command surface or plugin descriptors change, run:
|
||||||
|
|
||||||
- Generated-heavy paths:
|
- `./scripts/policy/check-command-surface.sh --strict`
|
||||||
- `docs/generated/`
|
- `./scripts/policy/check-open-source-assets.sh`
|
||||||
- `skills/generated/`
|
|
||||||
- `test/golden/generated_outputs/`
|
|
||||||
- When generator or command surface changes, run:
|
|
||||||
- `./scripts/policy/check-generated-drift.sh`
|
|
||||||
- `./scripts/policy/check-command-surface.sh --strict`
|
|
||||||
|
|
||||||
## Common Commands
|
## Common Commands
|
||||||
|
|
||||||
@@ -55,9 +59,6 @@ make build
|
|||||||
make test
|
make test
|
||||||
make lint
|
make lint
|
||||||
./scripts/dev/ci-local.sh
|
./scripts/dev/ci-local.sh
|
||||||
./scripts/policy/check-generated-drift.sh
|
|
||||||
./scripts/policy/check-command-surface.sh --strict
|
|
||||||
./scripts/policy/check-open-source-assets.sh
|
|
||||||
git diff --check
|
git diff --check
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
@@ -2,12 +2,11 @@
|
|||||||
|
|
||||||
Every runtime command the `dws` CLI exposes when loaded with the **pre** environment configuration.
|
Every runtime command the `dws` CLI exposes when loaded with the **pre** environment configuration.
|
||||||
|
|
||||||
- **Source**: `dws-wukong/envelope/channel/open/pre/config.json`
|
|
||||||
- **Products**: 13
|
- **Products**: 13
|
||||||
- **Total commands**: 160
|
- **Total commands**: 160
|
||||||
- **Generated from**: `internal/compat.BuildDynamicCommands` rendering of the pre config — the same code path the CLI uses at runtime.
|
- **Generated from**: `internal/plugin` command descriptors — the same code path the CLI uses at runtime.
|
||||||
|
|
||||||
> Auto-generated. Edit `pre/config.json`, not this file.
|
> Auto-generated. Update plugin descriptors in `internal/plugin/`, not this file.
|
||||||
|
|
||||||
## Global flags
|
## Global flags
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
# Running the connector as a 7x24 service
|
# Running the connector as a 7x24 service
|
||||||
|
|
||||||
`dws devapp robot connect` keeps a DingTalk robot wired to a local agent over a
|
`dws dev connect` keeps a DingTalk robot wired to a local agent over a
|
||||||
Stream long-connection. By default it runs in the foreground and dies when the
|
Stream long-connection. By default it runs in the foreground and dies when the
|
||||||
terminal closes. For an unattended "digital employee" you have two options.
|
terminal closes. For an unattended "digital employee" you have two options.
|
||||||
|
|
||||||
@@ -15,14 +15,14 @@ terminal closes. For an unattended "digital employee" you have two options.
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Detach into a background supervisor that restarts the connector if it crashes.
|
# Detach into a background supervisor that restarts the connector if it crashes.
|
||||||
dws devapp robot connect --daemon \
|
dws dev connect --daemon \
|
||||||
--channel claudecode \
|
--channel claudecode \
|
||||||
--unified-app-id <unifiedAppId>
|
--unified-app-id <unifiedAppId>
|
||||||
|
|
||||||
# Inspect / stop / restart it (locate the daemon by unifiedAppId).
|
# Inspect / stop / restart it (locate the daemon by unifiedAppId).
|
||||||
dws devapp robot connect status --unified-app-id <unifiedAppId>
|
dws dev connect status --unified-app-id <unifiedAppId>
|
||||||
dws devapp robot connect stop --unified-app-id <unifiedAppId>
|
dws dev connect stop --unified-app-id <unifiedAppId>
|
||||||
dws devapp robot connect restart --unified-app-id <unifiedAppId>
|
dws dev connect restart --unified-app-id <unifiedAppId>
|
||||||
```
|
```
|
||||||
|
|
||||||
- The parent prints the daemon pid and the log path, then exits.
|
- The parent prints the daemon pid and the log path, then exits.
|
||||||
@@ -60,8 +60,7 @@ and `REPLACE_UNIFIED_APP_ID`, then `launchctl load -w <path>`.
|
|||||||
<key>ProgramArguments</key>
|
<key>ProgramArguments</key>
|
||||||
<array>
|
<array>
|
||||||
<string>/usr/local/bin/dws</string>
|
<string>/usr/local/bin/dws</string>
|
||||||
<string>devapp</string>
|
<string>dev</string>
|
||||||
<string>robot</string>
|
|
||||||
<string>connect</string>
|
<string>connect</string>
|
||||||
<string>--channel</string>
|
<string>--channel</string>
|
||||||
<string>claudecode</string>
|
<string>claudecode</string>
|
||||||
@@ -110,7 +109,7 @@ Wants=network-online.target
|
|||||||
|
|
||||||
[Service]
|
[Service]
|
||||||
Type=simple
|
Type=simple
|
||||||
ExecStart=/usr/local/bin/dws devapp robot connect \
|
ExecStart=/usr/local/bin/dws dev connect \
|
||||||
--channel claudecode \
|
--channel claudecode \
|
||||||
--unified-app-id REPLACE_UNIFIED_APP_ID
|
--unified-app-id REPLACE_UNIFIED_APP_ID
|
||||||
Restart=always
|
Restart=always
|
||||||
@@ -136,7 +135,7 @@ security warning to stderr. This form:
|
|||||||
- exposes `clientSecret` to every user on the box via `ps -ef`;
|
- exposes `clientSecret` to every user on the box via `ps -ef`;
|
||||||
- gets baked into launchd `ProgramArguments` / systemd `ExecStart`, which
|
- gets baked into launchd `ProgramArguments` / systemd `ExecStart`, which
|
||||||
makes rotation harder;
|
makes rotation harder;
|
||||||
- means `dws devapp robot connect restart` cannot re-fetch credentials — you
|
- means `dws dev connect restart` cannot re-fetch credentials — you
|
||||||
must re-run the full command yourself.
|
must re-run the full command yourself.
|
||||||
|
|
||||||
Prefer `--unified-app-id`. Only fall back to the pair when you understand the
|
Prefer `--unified-app-id`. Only fall back to the pair when you understand the
|
||||||
|
|||||||
@@ -923,6 +923,9 @@ func resolveIdentityHeaders() map[string]string {
|
|||||||
if fn := edition.Get().MergeHeaders; fn != nil {
|
if fn := edition.Get().MergeHeaders; fn != nil {
|
||||||
headers = fn(headers)
|
headers = fn(headers)
|
||||||
}
|
}
|
||||||
|
if fn := edition.Get().EnterpriseCredentialHeaders; fn != nil {
|
||||||
|
headers = fn(headers)
|
||||||
|
}
|
||||||
return headers
|
return headers
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -29,6 +29,13 @@ func TestClassifyDenialReason(t *testing.T) {
|
|||||||
},
|
},
|
||||||
want: "channel_required",
|
want: "channel_required",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "error ENTERPRISE_NOT_AUTHORIZED",
|
||||||
|
status: &CLIAuthStatus{
|
||||||
|
ErrorCode: "ENTERPRISE_NOT_AUTHORIZED",
|
||||||
|
},
|
||||||
|
want: "enterprise_not_authorized",
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: "error NO_AUTH",
|
name: "error NO_AUTH",
|
||||||
status: &CLIAuthStatus{
|
status: &CLIAuthStatus{
|
||||||
|
|||||||
@@ -276,6 +276,14 @@ func (p *DeviceFlowProvider) loginOnce(ctx context.Context, attempt int) (*Token
|
|||||||
_, _ = fmt.Fprintln(p.output(), i18n.T(" 请升级到最新版本的 CLI 后重试。"))
|
_, _ = fmt.Fprintln(p.output(), i18n.T(" 请升级到最新版本的 CLI 后重试。"))
|
||||||
_, _ = fmt.Fprintln(p.output(), "")
|
_, _ = fmt.Fprintln(p.output(), "")
|
||||||
return nil, errors.New(i18n.T("当前组织已开启渠道管控,请升级到最新版本的 CLI 后重试"))
|
return nil, errors.New(i18n.T("当前组织已开启渠道管控,请升级到最新版本的 CLI 后重试"))
|
||||||
|
case "enterprise_not_authorized":
|
||||||
|
msg := i18n.T("本次请求未通过企业安全认证")
|
||||||
|
if authStatus != nil && strings.TrimSpace(authStatus.ErrorMsg) != "" {
|
||||||
|
msg = strings.TrimSpace(authStatus.ErrorMsg)
|
||||||
|
}
|
||||||
|
_, _ = fmt.Fprintln(p.output(), dfRed("⚠️ "+msg))
|
||||||
|
_, _ = fmt.Fprintln(p.output(), "")
|
||||||
|
return nil, errors.New(msg)
|
||||||
case "no_auth":
|
case "no_auth":
|
||||||
_, _ = fmt.Fprintln(p.output(), dfRed(i18n.T("⚠️ 认证已失效")))
|
_, _ = fmt.Fprintln(p.output(), dfRed(i18n.T("⚠️ 认证已失效")))
|
||||||
_, _ = fmt.Fprintln(p.output(), i18n.T(" 请执行 dws auth 重新登录。"))
|
_, _ = fmt.Fprintln(p.output(), i18n.T(" 请执行 dws auth 重新登录。"))
|
||||||
|
|||||||
@@ -0,0 +1,42 @@
|
|||||||
|
// Copyright 2026 Alibaba Group
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
package auth
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/pkg/edition"
|
||||||
|
)
|
||||||
|
|
||||||
|
// applyEditionEnterpriseCredentialHeaders injects overlay-provided enterprise
|
||||||
|
// credential headers (e.g. x-dws-enterprise-credential) into MCP control-plane
|
||||||
|
// and OAuth proxy requests.
|
||||||
|
func applyEditionEnterpriseCredentialHeaders(req *http.Request) {
|
||||||
|
if req == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
fn := edition.Get().EnterpriseCredentialHeaders
|
||||||
|
if fn == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
merged := fn(nil)
|
||||||
|
for k, v := range merged {
|
||||||
|
k = strings.TrimSpace(k)
|
||||||
|
v = strings.TrimSpace(v)
|
||||||
|
if k != "" && v != "" {
|
||||||
|
req.Header.Set(k, v)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -18,6 +18,7 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"html"
|
||||||
"io"
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
@@ -212,6 +213,7 @@ func (p *OAuthProvider) postJSON(ctx context.Context, endpoint string, body any)
|
|||||||
return nil, fmt.Errorf("creating request: %w", err)
|
return nil, fmt.Errorf("creating request: %w", err)
|
||||||
}
|
}
|
||||||
req.Header.Set("Content-Type", "application/json")
|
req.Header.Set("Content-Type", "application/json")
|
||||||
|
applyEditionEnterpriseCredentialHeaders(req)
|
||||||
|
|
||||||
client := p.httpClient
|
client := p.httpClient
|
||||||
if client == nil {
|
if client == nil {
|
||||||
@@ -1143,6 +1145,112 @@ const channelDeniedHTML = `<!doctype html>
|
|||||||
</body>
|
</body>
|
||||||
</html>`
|
</html>`
|
||||||
|
|
||||||
|
const enterpriseDeniedHTML = `<!doctype html>
|
||||||
|
<html lang="zh-CN">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
|
<title>钉钉 CLI</title>
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
font-family:
|
||||||
|
-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto,
|
||||||
|
"Helvetica Neue", Arial, sans-serif;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
min-height: 100vh;
|
||||||
|
margin: 0;
|
||||||
|
background: #f5f5f5;
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
.card {
|
||||||
|
height: 600px;
|
||||||
|
width: 480px;
|
||||||
|
border-radius: 16px;
|
||||||
|
background: #ffffff;
|
||||||
|
box-sizing: border-box;
|
||||||
|
border: 1px solid #f2f2f6;
|
||||||
|
box-shadow: 0px 2px 4px 0px rgba(0, 0, 0, 0.12);
|
||||||
|
padding: 32px 24px 24px;
|
||||||
|
text-align: center;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
.lock-icon {
|
||||||
|
width: 120px;
|
||||||
|
height: 120px;
|
||||||
|
margin: 0 auto;
|
||||||
|
object-fit: contain;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
h1 {
|
||||||
|
margin: 8px 0 0;
|
||||||
|
font-family:
|
||||||
|
"PingFang SC",
|
||||||
|
-apple-system,
|
||||||
|
BlinkMacSystemFont,
|
||||||
|
"Segoe UI",
|
||||||
|
Roboto,
|
||||||
|
"Helvetica Neue",
|
||||||
|
Arial,
|
||||||
|
sans-serif;
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: 600;
|
||||||
|
line-height: 44px;
|
||||||
|
text-align: center;
|
||||||
|
letter-spacing: normal;
|
||||||
|
color: #181c1f;
|
||||||
|
}
|
||||||
|
p {
|
||||||
|
margin: 0;
|
||||||
|
font-family:
|
||||||
|
"PingFang SC",
|
||||||
|
-apple-system,
|
||||||
|
BlinkMacSystemFont,
|
||||||
|
"Segoe UI",
|
||||||
|
Roboto,
|
||||||
|
"Helvetica Neue",
|
||||||
|
Arial,
|
||||||
|
sans-serif;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: normal;
|
||||||
|
line-height: 21px;
|
||||||
|
text-align: center;
|
||||||
|
letter-spacing: normal;
|
||||||
|
color: rgba(24, 28, 31, 0.6);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="card">
|
||||||
|
<img
|
||||||
|
class="lock-icon"
|
||||||
|
src="https://img.alicdn.com/imgextra/i4/O1CN01fS3xxz1vbzZSGjbe0_!!6000000006192-2-tps-480-480.png"
|
||||||
|
alt="lock icon"
|
||||||
|
/>
|
||||||
|
<h1>企业安全认证未通过</h1>
|
||||||
|
<p>__ENTERPRISE_DENIED_MSG__</p>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>`
|
||||||
|
|
||||||
|
// defaultEnterpriseDeniedMsg is shown when the server returns no errorMsg.
|
||||||
|
const defaultEnterpriseDeniedMsg = "本次请求未通过企业安全认证"
|
||||||
|
|
||||||
|
// renderEnterpriseDeniedHTML injects the server-provided denial message (falling
|
||||||
|
// back to the default text) into the enterprise-denied page. The message is
|
||||||
|
// HTML-escaped before insertion.
|
||||||
|
func renderEnterpriseDeniedHTML(serverMsg string) string {
|
||||||
|
msg := strings.TrimSpace(serverMsg)
|
||||||
|
if msg == "" {
|
||||||
|
msg = defaultEnterpriseDeniedMsg
|
||||||
|
}
|
||||||
|
return strings.ReplaceAll(enterpriseDeniedHTML, "__ENTERPRISE_DENIED_MSG__", html.EscapeString(msg)+" 此页面可以关闭。")
|
||||||
|
}
|
||||||
|
|
||||||
// CLIAuthStatus represents the response from /cli/cliAuthEnabled API.
|
// CLIAuthStatus represents the response from /cli/cliAuthEnabled API.
|
||||||
type CLIAuthStatus struct {
|
type CLIAuthStatus struct {
|
||||||
Success bool `json:"success"`
|
Success bool `json:"success"`
|
||||||
@@ -1179,6 +1287,9 @@ func classifyDenialReason(status *CLIAuthStatus, currentChannel string) string {
|
|||||||
if status.ErrorCode == "CHANNEL_REQUIRED" {
|
if status.ErrorCode == "CHANNEL_REQUIRED" {
|
||||||
return "channel_required"
|
return "channel_required"
|
||||||
}
|
}
|
||||||
|
if status.ErrorCode == "ENTERPRISE_NOT_AUTHORIZED" {
|
||||||
|
return "enterprise_not_authorized"
|
||||||
|
}
|
||||||
if status.ErrorCode == "NO_AUTH" {
|
if status.ErrorCode == "NO_AUTH" {
|
||||||
return "no_auth"
|
return "no_auth"
|
||||||
}
|
}
|
||||||
@@ -1268,6 +1379,7 @@ func (p *OAuthProvider) doCheckCLIAuthEnabled(ctx context.Context, accessToken s
|
|||||||
if ch := os.Getenv("DWS_CHANNEL"); ch != "" {
|
if ch := os.Getenv("DWS_CHANNEL"); ch != "" {
|
||||||
req.Header.Set("x-dws-channel", ch)
|
req.Header.Set("x-dws-channel", ch)
|
||||||
}
|
}
|
||||||
|
applyEditionEnterpriseCredentialHeaders(req)
|
||||||
|
|
||||||
client := p.httpClient
|
client := p.httpClient
|
||||||
if client == nil {
|
if client == nil {
|
||||||
@@ -1319,6 +1431,7 @@ func doGetSuperAdmins(ctx context.Context, accessToken string) (*SuperAdminRespo
|
|||||||
return nil, fmt.Errorf("creating request: %w", err)
|
return nil, fmt.Errorf("creating request: %w", err)
|
||||||
}
|
}
|
||||||
req.Header.Set("x-user-access-token", accessToken)
|
req.Header.Set("x-user-access-token", accessToken)
|
||||||
|
applyEditionEnterpriseCredentialHeaders(req)
|
||||||
|
|
||||||
resp, err := oauthHTTPClient.Do(req)
|
resp, err := oauthHTTPClient.Do(req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -1366,6 +1479,7 @@ func doSendCliAuthApply(ctx context.Context, accessToken, adminStaffID string) (
|
|||||||
return nil, fmt.Errorf("creating request: %w", err)
|
return nil, fmt.Errorf("creating request: %w", err)
|
||||||
}
|
}
|
||||||
req.Header.Set("x-user-access-token", accessToken)
|
req.Header.Set("x-user-access-token", accessToken)
|
||||||
|
applyEditionEnterpriseCredentialHeaders(req)
|
||||||
|
|
||||||
resp, err := oauthHTTPClient.Do(req)
|
resp, err := oauthHTTPClient.Do(req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ import (
|
|||||||
"net"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@@ -150,6 +151,7 @@ func (p *OAuthProvider) Login(ctx context.Context, force bool) (*TokenData, erro
|
|||||||
err error
|
err error
|
||||||
cliAuthDisabled bool
|
cliAuthDisabled bool
|
||||||
denialReason string
|
denialReason string
|
||||||
|
errorMsg string // server-provided errorMsg from /cli/cliAuthEnabled
|
||||||
}
|
}
|
||||||
resultCh := make(chan callbackResult, 1)
|
resultCh := make(chan callbackResult, 1)
|
||||||
errCh := make(chan error, 1)
|
errCh := make(chan error, 1)
|
||||||
@@ -270,6 +272,13 @@ func (p *OAuthProvider) Login(ctx context.Context, force bool) (*TokenData, erro
|
|||||||
}
|
}
|
||||||
cliAuthEnabled := denialReason == ""
|
cliAuthEnabled := denialReason == ""
|
||||||
|
|
||||||
|
// Server-provided errorMsg (nil-safe), surfaced both on the page and to
|
||||||
|
// the terminal so portal can update copy without releasing the CLI.
|
||||||
|
serverMsg := ""
|
||||||
|
if authStatus != nil {
|
||||||
|
serverMsg = authStatus.ErrorMsg
|
||||||
|
}
|
||||||
|
|
||||||
// Update CLI auth disabled state
|
// Update CLI auth disabled state
|
||||||
callbackTokenMu.Lock()
|
callbackTokenMu.Lock()
|
||||||
callbackAuthDisabled = !cliAuthEnabled
|
callbackAuthDisabled = !cliAuthEnabled
|
||||||
@@ -284,6 +293,8 @@ func (p *OAuthProvider) Login(ctx context.Context, force bool) (*TokenData, erro
|
|||||||
_, _ = fmt.Fprint(w, accessDeniedHTML)
|
_, _ = fmt.Fprint(w, accessDeniedHTML)
|
||||||
case denialReason == "channel_not_allowed" || denialReason == "channel_required":
|
case denialReason == "channel_not_allowed" || denialReason == "channel_required":
|
||||||
_, _ = fmt.Fprint(w, channelDeniedHTML)
|
_, _ = fmt.Fprint(w, channelDeniedHTML)
|
||||||
|
case denialReason == "enterprise_not_authorized":
|
||||||
|
_, _ = fmt.Fprint(w, renderEnterpriseDeniedHTML(serverMsg))
|
||||||
default:
|
default:
|
||||||
_, _ = fmt.Fprint(w, notEnabledHTML)
|
_, _ = fmt.Fprint(w, notEnabledHTML)
|
||||||
}
|
}
|
||||||
@@ -293,7 +304,7 @@ func (p *OAuthProvider) Login(ctx context.Context, force bool) (*TokenData, erro
|
|||||||
}
|
}
|
||||||
// Notify main goroutine with full result
|
// Notify main goroutine with full result
|
||||||
select {
|
select {
|
||||||
case resultCh <- callbackResult{token: tokenData, cliAuthDisabled: !cliAuthEnabled, denialReason: denialReason}:
|
case resultCh <- callbackResult{token: tokenData, cliAuthDisabled: !cliAuthEnabled, denialReason: denialReason, errorMsg: serverMsg}:
|
||||||
default:
|
default:
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -444,6 +455,11 @@ func (p *OAuthProvider) Login(ctx context.Context, force bool) (*TokenData, erro
|
|||||||
return nil, errors.New(i18n.T("您不在该组织的 CLI 授权人员范围内,请联系组织管理员将您加入授权名单"))
|
return nil, errors.New(i18n.T("您不在该组织的 CLI 授权人员范围内,请联系组织管理员将您加入授权名单"))
|
||||||
case "channel_not_allowed", "channel_required":
|
case "channel_not_allowed", "channel_required":
|
||||||
return nil, errors.New(i18n.T("当前渠道未获得该组织授权,或组织已开启渠道管控,请联系组织管理员开通渠道访问权限,或升级到最新版本的 CLI"))
|
return nil, errors.New(i18n.T("当前渠道未获得该组织授权,或组织已开启渠道管控,请联系组织管理员开通渠道访问权限,或升级到最新版本的 CLI"))
|
||||||
|
case "enterprise_not_authorized":
|
||||||
|
if msg := strings.TrimSpace(result.errorMsg); msg != "" {
|
||||||
|
return nil, errors.New(msg)
|
||||||
|
}
|
||||||
|
return nil, errors.New(i18n.T("本次请求未通过企业安全认证"))
|
||||||
}
|
}
|
||||||
|
|
||||||
_, _ = fmt.Fprintln(p.output(), "")
|
_, _ = fmt.Fprintln(p.output(), "")
|
||||||
|
|||||||
@@ -26,12 +26,12 @@ import (
|
|||||||
"syscall"
|
"syscall"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/cobracmd"
|
||||||
apperrors "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/errors"
|
apperrors "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/errors"
|
||||||
|
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/executor"
|
||||||
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/logging"
|
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/logging"
|
||||||
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/tui"
|
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/tui"
|
||||||
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/pkg/config"
|
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/pkg/config"
|
||||||
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/cobracmd"
|
|
||||||
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/executor"
|
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -61,13 +61,13 @@ const (
|
|||||||
// supervisor pid plus enough context for `status` to report without re-deriving
|
// supervisor pid plus enough context for `status` to report without re-deriving
|
||||||
// it (start time for uptime, log path, the dir key it was filed under).
|
// it (start time for uptime, log path, the dir key it was filed under).
|
||||||
type daemonState struct {
|
type daemonState struct {
|
||||||
Pid int `json:"pid"`
|
Pid int `json:"pid"`
|
||||||
StartUnix int64 `json:"startUnix"`
|
StartUnix int64 `json:"startUnix"`
|
||||||
LogPath string `json:"logPath"`
|
LogPath string `json:"logPath"`
|
||||||
DirKey string `json:"dirKey"`
|
DirKey string `json:"dirKey"`
|
||||||
ClientID string `json:"clientId,omitempty"`
|
ClientID string `json:"clientId,omitempty"`
|
||||||
UnifiedAppID string `json:"unifiedAppId,omitempty"`
|
UnifiedAppID string `json:"unifiedAppId,omitempty"`
|
||||||
Channel string `json:"channel,omitempty"`
|
Channel string `json:"channel,omitempty"`
|
||||||
NotifyStaffID string `json:"notifyStaffId,omitempty"`
|
NotifyStaffID string `json:"notifyStaffId,omitempty"`
|
||||||
// Profile records the --profile selector the connector was started with,
|
// Profile records the --profile selector the connector was started with,
|
||||||
// so `restart` re-fetches credentials against the same org instead of the
|
// so `restart` re-fetches credentials against the same org instead of the
|
||||||
@@ -112,7 +112,7 @@ func connectDaemonDir(dirKey string) (string, error) {
|
|||||||
|
|
||||||
func daemonPidPath(dir string) string { return filepath.Join(dir, "daemon.pid") }
|
func daemonPidPath(dir string) string { return filepath.Join(dir, "daemon.pid") }
|
||||||
func daemonStatePath(dir string) string { return filepath.Join(dir, "daemon-state.json") }
|
func daemonStatePath(dir string) string { return filepath.Join(dir, "daemon-state.json") }
|
||||||
func daemonLogPath(dir string) string { return filepath.Join(dir, "daemon.log") }
|
func daemonLogPath(dir string) string { return filepath.Join(dir, "daemon.log") }
|
||||||
|
|
||||||
// writeDaemonState atomically persists the daemon state to daemon-state.json
|
// writeDaemonState atomically persists the daemon state to daemon-state.json
|
||||||
// (persistent, survives supervisor exit) so restart/list can recover config.
|
// (persistent, survives supervisor exit) so restart/list can recover config.
|
||||||
@@ -641,9 +641,9 @@ func newDevAppRobotConnectStopCommand() *cobra.Command {
|
|||||||
// platform — no secrets stored on disk.
|
// platform — no secrets stored on disk.
|
||||||
func newDevAppRobotConnectRestartCommand() *cobra.Command {
|
func newDevAppRobotConnectRestartCommand() *cobra.Command {
|
||||||
cmd := &cobra.Command{
|
cmd := &cobra.Command{
|
||||||
Use: "restart",
|
Use: "restart",
|
||||||
Short: "重启连接器守护进程(通过持久化的 unifiedAppId 重新拉取密钥,无需本地存密钥)",
|
Short: "重启连接器守护进程(通过持久化的 unifiedAppId 重新拉取密钥,无需本地存密钥)",
|
||||||
Args: cobra.NoArgs,
|
Args: cobra.NoArgs,
|
||||||
DisableAutoGenTag: true,
|
DisableAutoGenTag: true,
|
||||||
RunE: func(cmd *cobra.Command, _ []string) error {
|
RunE: func(cmd *cobra.Command, _ []string) error {
|
||||||
dirKey, err := connectDaemonDirKeyFromFlags(cmd)
|
dirKey, err := connectDaemonDirKeyFromFlags(cmd)
|
||||||
|
|||||||
@@ -677,7 +677,7 @@ func connectAgentOptionsFromCommand(cmd *cobra.Command) connectAgentOptions {
|
|||||||
auditSheetTab = "Sheet1"
|
auditSheetTab = "Sheet1"
|
||||||
}
|
}
|
||||||
return connectAgentOptions{Model: model, WorkDir: workDir, Memory: memory,
|
return connectAgentOptions{Model: model, WorkDir: workDir, Memory: memory,
|
||||||
Timeout: time.Duration(agentTimeoutSec) * time.Second,
|
Timeout: time.Duration(agentTimeoutSec) * time.Second,
|
||||||
ReplyCard: replyCard, CardTemplate: cardTemplate,
|
ReplyCard: replyCard, CardTemplate: cardTemplate,
|
||||||
KnowledgeDir: knowledgeDir,
|
KnowledgeDir: knowledgeDir,
|
||||||
KnowledgeSource: knowledgeSource,
|
KnowledgeSource: knowledgeSource,
|
||||||
|
|||||||
@@ -85,6 +85,9 @@ type Hooks struct {
|
|||||||
// --- HTTP headers ---
|
// --- HTTP headers ---
|
||||||
MergeHeaders func(base map[string]string) map[string]string
|
MergeHeaders func(base map[string]string) map[string]string
|
||||||
|
|
||||||
|
// --- EnterpriseCredential HTTP headers ---
|
||||||
|
EnterpriseCredentialHeaders func(base map[string]string) map[string]string
|
||||||
|
|
||||||
// --- auth ---
|
// --- auth ---
|
||||||
AuthClientID string // OAuth client ID for device-flow authorisation
|
AuthClientID string // OAuth client ID for device-flow authorisation
|
||||||
AuthClientFromMCP bool // true → fetch client ID from MCP at runtime
|
AuthClientFromMCP bool // true → fetch client ID from MCP at runtime
|
||||||
|
|||||||
Reference in New Issue
Block a user