@dobby.ai/dobby 0.1.0 → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.example +0 -1
- package/AGENTS.md +7 -7
- package/README.md +64 -32
- package/config/gateway.example.json +10 -6
- package/dist/plugins/connector-discord/src/mapper.js +75 -0
- package/dist/src/cli/commands/doctor.js +81 -2
- package/dist/src/cli/commands/extension.js +3 -1
- package/dist/src/cli/commands/init.js +43 -173
- package/dist/src/cli/commands/topology.js +38 -14
- package/dist/src/cli/program.js +15 -131
- package/dist/src/cli/shared/config-io.js +3 -31
- package/dist/src/cli/shared/config-mutators.js +33 -9
- package/dist/src/cli/shared/configure-sections.js +52 -12
- package/dist/src/cli/shared/init-catalog.js +89 -46
- package/dist/src/cli/shared/local-extension-specs.js +85 -0
- package/dist/src/cli/shared/schema-prompts.js +26 -2
- package/dist/src/cli/tests/config-io.test.js +5 -5
- package/dist/src/cli/tests/discord-mapper.test.js +90 -0
- package/dist/src/cli/tests/doctor.test.js +145 -0
- package/dist/src/cli/tests/init-catalog.test.js +108 -61
- package/dist/src/cli/tests/program-options.test.js +14 -28
- package/dist/src/cli/tests/routing-config.test.js +59 -4
- package/dist/src/core/gateway.js +3 -1
- package/dist/src/core/routing.js +53 -38
- package/dist/src/main.js +0 -0
- package/dist/src/shared/dobby-repo.js +40 -0
- package/docs/RUNBOOK.md +28 -27
- package/package.json +3 -2
- package/plugins/connector-discord/package-lock.json +2 -2
- package/plugins/connector-discord/package.json +1 -1
- package/plugins/connector-discord/src/connector.ts +0 -5
- package/plugins/connector-discord/src/mapper.ts +3 -4
- package/plugins/connector-feishu/package-lock.json +2 -2
- package/plugins/connector-feishu/package.json +1 -1
- package/plugins/plugin-sdk/package-lock.json +2 -2
- package/plugins/plugin-sdk/package.json +1 -1
- package/plugins/provider-claude/package-lock.json +2 -2
- package/plugins/provider-claude/package.json +1 -1
- package/plugins/provider-claude-cli/package-lock.json +2 -2
- package/plugins/provider-claude-cli/package.json +1 -1
- package/plugins/provider-pi/package-lock.json +2 -2
- package/plugins/provider-pi/package.json +1 -1
- package/plugins/provider-pi/src/contribution.ts +139 -9
- package/src/cli/commands/doctor.ts +103 -2
- package/src/cli/commands/extension.ts +3 -1
- package/src/cli/commands/init.ts +45 -230
- package/src/cli/commands/topology.ts +48 -16
- package/src/cli/program.ts +16 -167
- package/src/cli/shared/config-io.ts +3 -35
- package/src/cli/shared/config-mutators.ts +39 -9
- package/src/cli/shared/config-types.ts +10 -2
- package/src/cli/shared/configure-sections.ts +55 -11
- package/src/cli/shared/init-catalog.ts +126 -66
- package/src/cli/shared/local-extension-specs.ts +108 -0
- package/src/cli/shared/schema-prompts.ts +30 -1
- package/src/cli/tests/config-io.test.ts +5 -5
- package/src/cli/tests/discord-mapper.test.ts +128 -0
- package/src/cli/tests/doctor.test.ts +149 -0
- package/src/cli/tests/init-catalog.test.ts +112 -64
- package/src/cli/tests/program-options.test.ts +14 -32
- package/src/cli/tests/routing-config.test.ts +76 -4
- package/src/core/gateway.ts +3 -1
- package/src/core/routing.ts +70 -45
- package/src/core/types.ts +8 -2
- package/src/shared/dobby-repo.ts +48 -0
- package/config/models.custom.example.json +0 -27
- package/dist/src/agent/tests/event-forwarder.test.js +0 -113
- package/dist/src/cli/shared/config-path.js +0 -207
- package/dist/src/cli/shared/init-models-file.js +0 -65
- package/dist/src/cli/shared/presets.js +0 -86
- package/dist/src/cli/tests/config-path.test.js +0 -21
- package/dist/src/cli/tests/discord-config.test.js +0 -23
- package/dist/src/cli/tests/presets.test.js +0 -41
- package/dist/src/cli/tests/routing-legacy.test.js +0 -191
- package/dist/src/core/tests/gateway-update-strategy.test.js +0 -167
- package/src/cli/shared/init-models-file.ts +0 -77
package/.env.example
CHANGED
package/AGENTS.md
CHANGED
|
@@ -68,7 +68,7 @@ npm run plugins:setup:local
|
|
|
68
68
|
- `src/cli/commands/start.ts`
|
|
69
69
|
- 启动网关、创建数据目录、加载扩展、实例化 provider / connector / sandbox、启动 cron 服务、接管优雅退出
|
|
70
70
|
- `src/cli/commands/init.ts`
|
|
71
|
-
-
|
|
71
|
+
- 首次初始化向导;会安装所需扩展并生成带占位符的 starter 配置模板
|
|
72
72
|
- `src/cli/commands/config*.ts`
|
|
73
73
|
- `config show|list|edit|schema *` 与 `configure`
|
|
74
74
|
- `src/cli/commands/topology.ts`
|
|
@@ -111,8 +111,8 @@ npm run plugins:setup:local
|
|
|
111
111
|
|
|
112
112
|
- `providers.default` 必须存在于 `providers.items`
|
|
113
113
|
- `sandboxes.default` 若存在且不是 `host.builtin`,必须存在于 `sandboxes.items`
|
|
114
|
-
- `routes.
|
|
115
|
-
- `routes.
|
|
114
|
+
- `routes.default.provider` 若未设置,运行时回落到 `providers.default`
|
|
115
|
+
- `routes.default.sandbox` 若未设置,运行时回落到 `sandboxes.default ?? host.builtin`
|
|
116
116
|
- 每条 route 在加载后都会补全 `provider`、`sandbox`、`tools`、`mentions`
|
|
117
117
|
- `bindings.items[*].connector` 必须指向存在的 `connectors.items`
|
|
118
118
|
- `bindings.items[*].route` 必须指向存在的 `routes.items`
|
|
@@ -133,8 +133,8 @@ npm run plugins:setup:local
|
|
|
133
133
|
- `connectorId + platform + accountId + chatId + messageId`
|
|
134
134
|
- 线程路由规则:
|
|
135
135
|
- Discord 线程消息使用父频道 ID 查 `bindings.items`
|
|
136
|
-
- Discord
|
|
137
|
-
- DM
|
|
136
|
+
- Discord guild channel 仍按显式 binding 匹配
|
|
137
|
+
- DM 会携带 `isDirectMessage` 进入 gateway,并可通过 `bindings.default` 回落到默认 route
|
|
138
138
|
- mention 策略:
|
|
139
139
|
- `mentions="required"` 时,群聊消息必须 @bot 才会进入 runtime
|
|
140
140
|
- 控制命令:
|
|
@@ -159,7 +159,7 @@ npm run plugins:setup:local
|
|
|
159
159
|
- 运行时加载来源只有 `<data.rootDir>/extensions/node_modules`
|
|
160
160
|
- 宿主不会从自身依赖树、`plugins/*` 源码目录或 `dist` 外路径 fallback
|
|
161
161
|
- `configSchema` 是可选的
|
|
162
|
-
- `
|
|
162
|
+
- `configure`、`config edit` 会优先按 schema 交互提问
|
|
163
163
|
- `applyAndValidateContributionSchemas` 会用 Ajv 套默认值并验证实例配置
|
|
164
164
|
|
|
165
165
|
当前仓库内的扩展源码与 contribution:
|
|
@@ -172,7 +172,7 @@ npm run plugins:setup:local
|
|
|
172
172
|
|
|
173
173
|
注意:
|
|
174
174
|
|
|
175
|
-
- `dobby init` 当前只内建选择 `provider.pi`、`provider.claude-cli` 和 `connector.
|
|
175
|
+
- `dobby init` 当前只内建选择 `provider.pi`、`provider.claude-cli`、`connector.discord` 和 `connector.feishu`
|
|
176
176
|
- `provider.claude` 与 sandbox 扩展需要手工安装 / 启用 / 配置
|
|
177
177
|
|
|
178
178
|
## 7. Cron / 计划任务约束
|
package/README.md
CHANGED
|
@@ -17,14 +17,14 @@ Discord-first 本地 Agent Gateway。宿主只负责 CLI、网关主流程、扩
|
|
|
17
17
|
|
|
18
18
|
- connector source -> binding -> route -> provider / sandbox
|
|
19
19
|
- Discord 频道 / 线程接入;线程消息继续按父频道命中 binding
|
|
20
|
-
- Feishu 长连接消息接入(self-built app
|
|
20
|
+
- Feishu 长连接消息接入(self-built app)
|
|
21
21
|
- Feishu 出站支持普通文本和 Markdown 卡片;默认群内直发,不走 reply thread
|
|
22
22
|
- conversation 级 runtime 复用与串行化
|
|
23
23
|
- 扩展 store 安装、启用、列举与 schema 驱动配置
|
|
24
24
|
- Discord 流式回复、typing、附件下载与图片输入
|
|
25
25
|
- cron 调度:一次性、固定间隔、cron expression
|
|
26
|
-
- 交互式初始化:`dobby init
|
|
27
|
-
-
|
|
26
|
+
- 交互式初始化:`dobby init`(支持多 provider / 多 connector starter)
|
|
27
|
+
- 配置检查与 schema inspect:`dobby config show|list|schema`
|
|
28
28
|
- 诊断与保守修复:`dobby doctor [--fix]`
|
|
29
29
|
|
|
30
30
|
## 架构概览
|
|
@@ -76,7 +76,7 @@ npm install
|
|
|
76
76
|
npm run build
|
|
77
77
|
```
|
|
78
78
|
|
|
79
|
-
3.
|
|
79
|
+
3. 初始化模板配置
|
|
80
80
|
|
|
81
81
|
```bash
|
|
82
82
|
npm run start -- init
|
|
@@ -84,21 +84,41 @@ npm run start -- init
|
|
|
84
84
|
|
|
85
85
|
`init` 会做这些事情:
|
|
86
86
|
|
|
87
|
-
- 交互选择 provider 和 connector
|
|
87
|
+
- 交互选择 provider 和 connector(均可多选)
|
|
88
88
|
- 自动安装所选扩展到运行时 extension store
|
|
89
|
-
-
|
|
89
|
+
- 写入一份带占位符的 `gateway.json` 模板
|
|
90
|
+
- 把 `routes.default.projectRoot` 设为当前工作目录
|
|
91
|
+
- 为 direct message 生成 `bindings.default`,回落到默认 route
|
|
92
|
+
- 为每个所选 connector 生成一个默认 binding 到同一条 route
|
|
90
93
|
- 生成 `gateway.json`
|
|
91
|
-
-
|
|
94
|
+
- `provider.pi` 默认写入最小 inline 配置,不再依赖 `models.custom.json`
|
|
92
95
|
|
|
93
|
-
说明:当前 `init`
|
|
96
|
+
说明:当前 `init` 内建这些 starter 选择:
|
|
94
97
|
|
|
95
|
-
|
|
98
|
+
- provider:`provider.pi`、`provider.claude-cli`
|
|
99
|
+
- connector:`connector.discord`、`connector.feishu`
|
|
100
|
+
|
|
101
|
+
4. 编辑 `gateway.json`
|
|
102
|
+
|
|
103
|
+
把 `REPLACE_WITH_*` / `YOUR_*` 占位值替换成你的真实配置,例如:
|
|
104
|
+
|
|
105
|
+
- `connectors.items[*]` 中的 token / appId / appSecret
|
|
106
|
+
- `bindings.items[*].source.id`
|
|
107
|
+
- `routes.items[*].projectRoot`(如需覆盖默认 project root)
|
|
108
|
+
|
|
109
|
+
5. 运行诊断
|
|
96
110
|
|
|
97
111
|
```bash
|
|
98
112
|
npm run start -- doctor
|
|
99
113
|
```
|
|
100
114
|
|
|
101
|
-
|
|
115
|
+
`doctor` 会同时检查:
|
|
116
|
+
|
|
117
|
+
- 配置结构 / 引用关系
|
|
118
|
+
- 缺失的扩展安装
|
|
119
|
+
- `REPLACE_WITH_*` / `YOUR_*` 这类 init 占位值是否还未替换
|
|
120
|
+
|
|
121
|
+
6. 启动网关
|
|
102
122
|
|
|
103
123
|
```bash
|
|
104
124
|
npm run start --
|
|
@@ -107,7 +127,9 @@ npm run start --
|
|
|
107
127
|
说明:
|
|
108
128
|
|
|
109
129
|
- `dobby` 无子命令时,默认等价于 `dobby start`
|
|
130
|
+
- `dobby --version` 可直接查看当前 CLI 版本
|
|
110
131
|
- 在仓库内直接运行时,CLI 会自动使用 `./config/gateway.json`
|
|
132
|
+
- 在仓库内执行 `init` / `extension install` 时,会优先安装 `plugins/*` 的本地构建产物
|
|
111
133
|
- 也可以通过环境变量覆盖配置路径:
|
|
112
134
|
|
|
113
135
|
```bash
|
|
@@ -152,33 +174,23 @@ cron 配置路径优先级:
|
|
|
152
174
|
顶层命令:
|
|
153
175
|
|
|
154
176
|
```bash
|
|
177
|
+
dobby --version
|
|
155
178
|
dobby start
|
|
156
179
|
dobby init
|
|
157
|
-
dobby configure
|
|
158
180
|
dobby doctor [--fix]
|
|
159
181
|
```
|
|
160
182
|
|
|
161
|
-
|
|
183
|
+
配置检查:
|
|
162
184
|
|
|
163
185
|
```bash
|
|
164
186
|
dobby config show [section] [--json]
|
|
165
187
|
dobby config list [section] [--json]
|
|
166
|
-
dobby config edit [--section provider|connector|route|binding]
|
|
167
188
|
dobby config schema list [--json]
|
|
168
189
|
dobby config schema show <contributionId> [--json]
|
|
169
|
-
|
|
170
|
-
dobby bot list [--json]
|
|
171
|
-
dobby bot set <connectorId> [--name <name>] [--token <token>]
|
|
172
|
-
|
|
173
|
-
dobby binding list [--connector <id>] [--json]
|
|
174
|
-
dobby binding set <bindingId> --connector <id> --source-type channel|chat --source-id <id> --route <id>
|
|
175
|
-
dobby binding remove <bindingId>
|
|
176
|
-
|
|
177
|
-
dobby route list [--json]
|
|
178
|
-
dobby route set <routeId> [--project-root <path>] [--tools full|readonly] [--provider <id>] [--sandbox <id>] [--mentions required|optional]
|
|
179
|
-
dobby route remove <routeId> [--cascade-bindings]
|
|
180
190
|
```
|
|
181
191
|
|
|
192
|
+
配置变更建议直接编辑 `gateway.json`,再通过 `dobby doctor` 或 `dobby start` 做校验。
|
|
193
|
+
|
|
182
194
|
扩展管理:
|
|
183
195
|
|
|
184
196
|
```bash
|
|
@@ -221,15 +233,17 @@ dobby cron remove <jobId>
|
|
|
221
233
|
- 默认 provider instance ID
|
|
222
234
|
- `providers.items[*].type` / `connectors.items[*].type` / `sandboxes.items[*].type`
|
|
223
235
|
- 指向某个 contribution,实例配置直接内联在对象里
|
|
224
|
-
- `routes.
|
|
225
|
-
- 统一提供 route 默认的 `provider`、`sandbox`、`tools`、`mentions`
|
|
236
|
+
- `routes.default`
|
|
237
|
+
- 统一提供 route 默认的 `projectRoot`、`provider`、`sandbox`、`tools`、`mentions`
|
|
226
238
|
- `routes.items[*]`
|
|
227
|
-
- route 是可复用的执行 profile
|
|
239
|
+
- route 是可复用的执行 profile,可继承默认 `projectRoot`,并按需覆盖 `provider`、`sandbox`、`tools`、`mentions`、`systemPromptFile`
|
|
240
|
+
- `bindings.default`
|
|
241
|
+
- direct message 未命中显式 binding 时使用的默认 route fallback
|
|
228
242
|
- `bindings.items[*]`
|
|
229
243
|
- `(connector, source.type, source.id) -> route` 的入口绑定
|
|
230
244
|
- `sandboxes.default`
|
|
231
245
|
- 未指定时默认使用 `host.builtin`
|
|
232
|
-
- 未匹配 binding
|
|
246
|
+
- 未匹配 binding 的入站消息会被直接忽略;仅 direct message 可回落到 `bindings.default`
|
|
233
247
|
|
|
234
248
|
当前代码还保留但未真正生效的字段:
|
|
235
249
|
|
|
@@ -239,7 +253,24 @@ dobby cron remove <jobId>
|
|
|
239
253
|
|
|
240
254
|
- gateway:[`config/gateway.example.json`](config/gateway.example.json)
|
|
241
255
|
- cron:[`config/cron.example.json`](config/cron.example.json)
|
|
242
|
-
|
|
256
|
+
|
|
257
|
+
`provider.pi` 现在使用 inline custom provider 配置。最小常用字段是:
|
|
258
|
+
|
|
259
|
+
- `model`
|
|
260
|
+
- `baseUrl`
|
|
261
|
+
- `apiKey`
|
|
262
|
+
|
|
263
|
+
这些字段默认自动补齐:
|
|
264
|
+
|
|
265
|
+
- `provider = "custom-openai"`
|
|
266
|
+
- `api = "openai-completions"`
|
|
267
|
+
- `authHeader = false`
|
|
268
|
+
- `thinkingLevel = "off"`
|
|
269
|
+
- `models = [{ id: model }]`
|
|
270
|
+
|
|
271
|
+
只有在你需要多模型元数据或覆盖能力参数时,才需要手工展开 `models`。
|
|
272
|
+
|
|
273
|
+
`apiKey` 支持直接写 literal,也支持写环境变量名,由 `pi` 的 `AuthStorage` / `ModelRegistry` 按上游规则解析。
|
|
243
274
|
|
|
244
275
|
## 扩展包与 contribution
|
|
245
276
|
|
|
@@ -255,7 +286,7 @@ dobby cron remove <jobId>
|
|
|
255
286
|
`dobby init` 当前只内建这些 starter 选择:
|
|
256
287
|
|
|
257
288
|
- provider:`provider.pi`、`provider.claude-cli`
|
|
258
|
-
- connector:`connector.discord`
|
|
289
|
+
- connector:`connector.discord`、`connector.feishu`
|
|
259
290
|
|
|
260
291
|
`provider.claude` 与 sandbox 相关扩展需要手工安装和配置,例如:
|
|
261
292
|
|
|
@@ -298,7 +329,8 @@ npm run start -- cron add daily-report \
|
|
|
298
329
|
|
|
299
330
|
## Discord 连接器的当前行为
|
|
300
331
|
|
|
301
|
-
-
|
|
332
|
+
- guild channel 仍按显式 binding 匹配
|
|
333
|
+
- DM 可通过 `bindings.default` 回落到默认 route
|
|
302
334
|
- 线程消息使用父频道 ID 做 binding 查找
|
|
303
335
|
- 会自动下载附件到本地
|
|
304
336
|
- 图片会作为 image input 传给 provider
|
|
@@ -372,7 +404,7 @@ npm run plugins:build
|
|
|
372
404
|
|
|
373
405
|
- `npm run dev:local` 与 `npm run start:local` 会尝试读取 `.env`
|
|
374
406
|
- 普通 `npm run start -- ...` 不会自动载入 `.env`
|
|
375
|
-
-
|
|
407
|
+
- `dobby init` 生成的是模板配置;运行前先替换 `gateway.json` 中的 placeholder
|
|
376
408
|
|
|
377
409
|
## 相关文档
|
|
378
410
|
|
|
@@ -24,10 +24,9 @@
|
|
|
24
24
|
"items": {
|
|
25
25
|
"pi.main": {
|
|
26
26
|
"type": "provider.pi",
|
|
27
|
-
"
|
|
28
|
-
"
|
|
29
|
-
"
|
|
30
|
-
"modelsFile": "./models.custom.json"
|
|
27
|
+
"model": "REPLACE_WITH_PROVIDER_MODEL_ID",
|
|
28
|
+
"baseUrl": "REPLACE_WITH_PROVIDER_BASE_URL",
|
|
29
|
+
"apiKey": "REPLACE_WITH_PROVIDER_API_KEY_OR_ENV"
|
|
31
30
|
}
|
|
32
31
|
}
|
|
33
32
|
},
|
|
@@ -47,19 +46,24 @@
|
|
|
47
46
|
"items": {}
|
|
48
47
|
},
|
|
49
48
|
"routes": {
|
|
50
|
-
"
|
|
49
|
+
"default": {
|
|
50
|
+
"projectRoot": "/Users/you/workspace/dobby",
|
|
51
51
|
"provider": "pi.main",
|
|
52
52
|
"sandbox": "host.builtin",
|
|
53
53
|
"tools": "full",
|
|
54
54
|
"mentions": "required"
|
|
55
55
|
},
|
|
56
56
|
"items": {
|
|
57
|
+
"main": {},
|
|
57
58
|
"projectA": {
|
|
58
59
|
"projectRoot": "/Users/you/workspace/project-a"
|
|
59
60
|
}
|
|
60
61
|
}
|
|
61
62
|
},
|
|
62
63
|
"bindings": {
|
|
64
|
+
"default": {
|
|
65
|
+
"route": "main"
|
|
66
|
+
},
|
|
63
67
|
"items": {
|
|
64
68
|
"discord.main.projectA": {
|
|
65
69
|
"connector": "discord.main",
|
|
@@ -125,4 +129,4 @@
|
|
|
125
129
|
"rootDir": "./data",
|
|
126
130
|
"dedupTtlMs": 604800000
|
|
127
131
|
}
|
|
128
|
-
}
|
|
132
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { mkdir, writeFile } from "node:fs/promises";
|
|
2
|
+
import { dirname, join } from "node:path";
|
|
3
|
+
function stripBotMention(text, botUserId) {
|
|
4
|
+
const mentionRegex = new RegExp(`<@!?${botUserId}>`, "g");
|
|
5
|
+
return text.replace(mentionRegex, "").trim();
|
|
6
|
+
}
|
|
7
|
+
function sanitizeFileName(value) {
|
|
8
|
+
return value.replaceAll(/[^a-zA-Z0-9._-]/g, "_");
|
|
9
|
+
}
|
|
10
|
+
async function downloadAttachment(url, targetPath) {
|
|
11
|
+
const response = await fetch(url);
|
|
12
|
+
if (!response.ok) {
|
|
13
|
+
throw new Error(`Failed to download attachment from ${url}: ${response.status}`);
|
|
14
|
+
}
|
|
15
|
+
const data = await response.arrayBuffer();
|
|
16
|
+
await mkdir(dirname(targetPath), { recursive: true });
|
|
17
|
+
await writeFile(targetPath, Buffer.from(data));
|
|
18
|
+
}
|
|
19
|
+
function mapAttachmentBase(messageAttachment) {
|
|
20
|
+
return {
|
|
21
|
+
id: messageAttachment.id,
|
|
22
|
+
size: messageAttachment.size,
|
|
23
|
+
remoteUrl: messageAttachment.url,
|
|
24
|
+
...(messageAttachment.name ? { fileName: messageAttachment.name } : {}),
|
|
25
|
+
...(messageAttachment.contentType ? { mimeType: messageAttachment.contentType } : {}),
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
export async function mapDiscordMessage(message, connectorId, botUserId, sourceId, attachmentsRoot, logger) {
|
|
29
|
+
if (message.author.bot)
|
|
30
|
+
return null;
|
|
31
|
+
const isDirectMessage = message.guildId == null;
|
|
32
|
+
const mentionedBot = message.mentions.users.has(botUserId);
|
|
33
|
+
const chatId = message.channelId;
|
|
34
|
+
const threadId = message.channel.isThread() ? message.channelId : undefined;
|
|
35
|
+
const cleanedText = stripBotMention(message.content ?? "", botUserId);
|
|
36
|
+
const attachments = [];
|
|
37
|
+
for (const attachment of message.attachments.values()) {
|
|
38
|
+
const base = mapAttachmentBase(attachment);
|
|
39
|
+
const attachmentDir = join(attachmentsRoot, sourceId, message.id);
|
|
40
|
+
const fileName = sanitizeFileName(attachment.name ?? attachment.id);
|
|
41
|
+
const localPath = join(attachmentDir, fileName);
|
|
42
|
+
try {
|
|
43
|
+
await downloadAttachment(attachment.url, localPath);
|
|
44
|
+
attachments.push({
|
|
45
|
+
...base,
|
|
46
|
+
localPath,
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
catch (error) {
|
|
50
|
+
logger.warn({ err: error, attachmentUrl: attachment.url }, "Failed to download Discord attachment; keeping metadata only");
|
|
51
|
+
attachments.push(base);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
return {
|
|
55
|
+
connectorId,
|
|
56
|
+
platform: "discord",
|
|
57
|
+
accountId: botUserId,
|
|
58
|
+
source: {
|
|
59
|
+
type: "channel",
|
|
60
|
+
id: sourceId,
|
|
61
|
+
},
|
|
62
|
+
chatId,
|
|
63
|
+
messageId: message.id,
|
|
64
|
+
userId: message.author.id,
|
|
65
|
+
userName: message.author.username,
|
|
66
|
+
text: cleanedText,
|
|
67
|
+
attachments,
|
|
68
|
+
timestampMs: message.createdTimestamp,
|
|
69
|
+
raw: message.toJSON(),
|
|
70
|
+
isDirectMessage,
|
|
71
|
+
mentionedBot,
|
|
72
|
+
...(message.guildId ? { guildId: message.guildId } : {}),
|
|
73
|
+
...(threadId ? { threadId } : {}),
|
|
74
|
+
};
|
|
75
|
+
}
|
|
@@ -7,6 +7,33 @@ import { ensureGatewayConfigShape, setDefaultProviderIfMissingOrInvalid, } from
|
|
|
7
7
|
import { DISCORD_CONNECTOR_CONTRIBUTION_ID } from "../shared/discord-config.js";
|
|
8
8
|
import { readRawConfig, resolveConfigPath, resolveDataRootDir, writeConfigWithValidation } from "../shared/config-io.js";
|
|
9
9
|
import { createLogger } from "../shared/runtime.js";
|
|
10
|
+
function isPlaceholderValue(value) {
|
|
11
|
+
if (typeof value !== "string") {
|
|
12
|
+
return false;
|
|
13
|
+
}
|
|
14
|
+
const normalized = value.trim().toUpperCase();
|
|
15
|
+
return normalized.includes("REPLACE_WITH_") || normalized.includes("YOUR_");
|
|
16
|
+
}
|
|
17
|
+
function isCredentialLikeKey(key) {
|
|
18
|
+
return /(?:token|secret|api[-_]?key|appid|appsecret)/i.test(key);
|
|
19
|
+
}
|
|
20
|
+
function walkPlaceholders(value, path) {
|
|
21
|
+
if (isPlaceholderValue(value)) {
|
|
22
|
+
return [{ path, value }];
|
|
23
|
+
}
|
|
24
|
+
if (Array.isArray(value)) {
|
|
25
|
+
return value.flatMap((item, index) => walkPlaceholders(item, `${path}[${index}]`));
|
|
26
|
+
}
|
|
27
|
+
if (!value || typeof value !== "object") {
|
|
28
|
+
return [];
|
|
29
|
+
}
|
|
30
|
+
return Object.entries(value).flatMap(([key, nested]) => walkPlaceholders(nested, `${path}.${key}`));
|
|
31
|
+
}
|
|
32
|
+
function lastPathSegment(path) {
|
|
33
|
+
const withoutIndexes = path.replaceAll(/\[\d+\]/g, "");
|
|
34
|
+
const segments = withoutIndexes.split(".");
|
|
35
|
+
return segments[segments.length - 1] ?? withoutIndexes;
|
|
36
|
+
}
|
|
10
37
|
function expandHome(value) {
|
|
11
38
|
if (value === "~") {
|
|
12
39
|
return homedir();
|
|
@@ -76,6 +103,15 @@ export async function runDoctorCommand(options) {
|
|
|
76
103
|
message: `providers.items['${instanceId}'] references missing contribution '${instance.type}'`,
|
|
77
104
|
});
|
|
78
105
|
}
|
|
106
|
+
for (const hit of walkPlaceholders(instance, `providers.items['${instanceId}']`)) {
|
|
107
|
+
if (hit.path.endsWith(".type")) {
|
|
108
|
+
continue;
|
|
109
|
+
}
|
|
110
|
+
issues.push({
|
|
111
|
+
level: isCredentialLikeKey(lastPathSegment(hit.path)) ? "error" : "warning",
|
|
112
|
+
message: `${hit.path} still uses placeholder value '${hit.value}'`,
|
|
113
|
+
});
|
|
114
|
+
}
|
|
79
115
|
}
|
|
80
116
|
for (const [instanceId, instance] of Object.entries(normalized.connectors.items)) {
|
|
81
117
|
if (!availableContributionIds.has(instance.type)) {
|
|
@@ -84,6 +120,15 @@ export async function runDoctorCommand(options) {
|
|
|
84
120
|
message: `connectors.items['${instanceId}'] references missing contribution '${instance.type}'`,
|
|
85
121
|
});
|
|
86
122
|
}
|
|
123
|
+
for (const hit of walkPlaceholders(instance, `connectors.items['${instanceId}']`)) {
|
|
124
|
+
if (hit.path.endsWith(".type")) {
|
|
125
|
+
continue;
|
|
126
|
+
}
|
|
127
|
+
issues.push({
|
|
128
|
+
level: isCredentialLikeKey(lastPathSegment(hit.path)) ? "error" : "warning",
|
|
129
|
+
message: `${hit.path} still uses placeholder value '${hit.value}'`,
|
|
130
|
+
});
|
|
131
|
+
}
|
|
87
132
|
if (instance.type === DISCORD_CONNECTOR_CONTRIBUTION_ID) {
|
|
88
133
|
const botName = typeof instance.botName === "string" ? instance.botName.trim() : "";
|
|
89
134
|
const botToken = typeof instance.botToken === "string" ? instance.botToken.trim() : "";
|
|
@@ -109,18 +154,46 @@ export async function runDoctorCommand(options) {
|
|
|
109
154
|
});
|
|
110
155
|
}
|
|
111
156
|
}
|
|
157
|
+
if (normalized.routes.default.projectRoot && isPlaceholderValue(normalized.routes.default.projectRoot)) {
|
|
158
|
+
issues.push({
|
|
159
|
+
level: "warning",
|
|
160
|
+
message: `routes.default.projectRoot still uses placeholder value '${normalized.routes.default.projectRoot}'`,
|
|
161
|
+
});
|
|
162
|
+
}
|
|
112
163
|
for (const [routeId, route] of Object.entries(normalized.routes.items)) {
|
|
164
|
+
const effectiveProjectRoot = route.projectRoot ?? normalized.routes.default.projectRoot;
|
|
165
|
+
const projectRootSource = route.projectRoot ? `routes.items['${routeId}'].projectRoot` : "routes.default.projectRoot";
|
|
166
|
+
if (!effectiveProjectRoot) {
|
|
167
|
+
issues.push({
|
|
168
|
+
level: "error",
|
|
169
|
+
message: `routes.items['${routeId}'].projectRoot is required when routes.default.projectRoot is not set`,
|
|
170
|
+
});
|
|
171
|
+
continue;
|
|
172
|
+
}
|
|
173
|
+
if (isPlaceholderValue(effectiveProjectRoot)) {
|
|
174
|
+
issues.push({
|
|
175
|
+
level: "warning",
|
|
176
|
+
message: `${projectRootSource} still uses placeholder value '${effectiveProjectRoot}'`,
|
|
177
|
+
});
|
|
178
|
+
continue;
|
|
179
|
+
}
|
|
113
180
|
try {
|
|
114
|
-
const projectRootPath = resolveRouteProjectRoot(configPath,
|
|
181
|
+
const projectRootPath = resolveRouteProjectRoot(configPath, effectiveProjectRoot);
|
|
115
182
|
await access(projectRootPath);
|
|
116
183
|
}
|
|
117
184
|
catch {
|
|
118
185
|
issues.push({
|
|
119
186
|
level: "warning",
|
|
120
|
-
message:
|
|
187
|
+
message: `${projectRootSource} does not exist: ${effectiveProjectRoot}`,
|
|
121
188
|
});
|
|
122
189
|
}
|
|
123
190
|
}
|
|
191
|
+
if (normalized.bindings.default && !normalized.routes.items[normalized.bindings.default.route]) {
|
|
192
|
+
issues.push({
|
|
193
|
+
level: "error",
|
|
194
|
+
message: `bindings.default.route references unknown route '${normalized.bindings.default.route}'`,
|
|
195
|
+
});
|
|
196
|
+
}
|
|
124
197
|
const seenBindingSources = new Map();
|
|
125
198
|
for (const [bindingId, binding] of Object.entries(normalized.bindings.items)) {
|
|
126
199
|
if (!normalized.connectors.items[binding.connector]) {
|
|
@@ -135,6 +208,12 @@ export async function runDoctorCommand(options) {
|
|
|
135
208
|
message: `bindings.items['${bindingId}'].route references unknown route '${binding.route}'`,
|
|
136
209
|
});
|
|
137
210
|
}
|
|
211
|
+
if (isPlaceholderValue(binding.source.id)) {
|
|
212
|
+
issues.push({
|
|
213
|
+
level: "warning",
|
|
214
|
+
message: `bindings.items['${bindingId}'].source.id still uses placeholder value '${binding.source.id}'`,
|
|
215
|
+
});
|
|
216
|
+
}
|
|
138
217
|
const bindingKey = `${binding.connector}:${binding.source.type}:${binding.source.id}`;
|
|
139
218
|
const existingBindingId = seenBindingSources.get(bindingKey);
|
|
140
219
|
if (existingBindingId) {
|
|
@@ -3,6 +3,7 @@ import { loadGatewayConfig } from "../../core/routing.js";
|
|
|
3
3
|
import { ExtensionStoreManager } from "../../extension/manager.js";
|
|
4
4
|
import { applyContributionTemplates, buildContributionTemplates, ensureGatewayConfigShape, listContributionIds, setDefaultProviderIfMissingOrInvalid, upsertAllowListPackage, } from "../shared/config-mutators.js";
|
|
5
5
|
import { readRawConfig, requireRawConfig, resolveConfigPath, resolveDataRootDir, writeConfigWithValidation } from "../shared/config-io.js";
|
|
6
|
+
import { resolveExtensionInstallSpecs } from "../shared/local-extension-specs.js";
|
|
6
7
|
import { createLogger } from "../shared/runtime.js";
|
|
7
8
|
/**
|
|
8
9
|
* Resolves extension store directory from normalized gateway config.
|
|
@@ -25,7 +26,8 @@ export async function runExtensionInstallCommand(options) {
|
|
|
25
26
|
const logger = createLogger();
|
|
26
27
|
const rawConfig = (await readRawConfig(configPath)) ?? {};
|
|
27
28
|
const manager = new ExtensionStoreManager(logger, extensionStoreDirFromRaw(configPath, rawConfig));
|
|
28
|
-
const
|
|
29
|
+
const [resolvedSpec] = await resolveExtensionInstallSpecs([options.spec]);
|
|
30
|
+
const installed = await manager.install(resolvedSpec ?? options.spec);
|
|
29
31
|
if (!options.enable) {
|
|
30
32
|
const templates = buildContributionTemplates(installed.manifest.contributions);
|
|
31
33
|
if (options.json) {
|