dsh-apis-plugin 0.1.1 → 0.1.3

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/api-tools.js CHANGED
@@ -6,16 +6,31 @@ import { readFileSync, writeFileSync } from 'node:fs'
6
6
  import { join } from 'node:path'
7
7
  import Schema from '@deepseek-ai/schemastery'
8
8
 
9
- // 读取目录下 apis.txt 作为默认接口列表;支持空行与 # 注释
10
- function loadApisTxt(dir) {
11
- if (!dir) return []
9
+ // 读取目录下 api.cfg(key=value 行,# 注释):baseUrl 为服务前缀;apis 为接口列表,
10
+ // 支持逗号分隔同行书写,或 apis= 下面逐行一个接口
11
+ function loadApiCfg(dir) {
12
+ const empty = { apis: [] }
13
+ if (!dir) return empty
12
14
  try {
13
- return readFileSync(join(dir, 'apis.txt'), 'utf8')
14
- .split(/\r?\n/)
15
- .map((line) => line.trim())
16
- .filter((line) => line && !line.startsWith('#'))
15
+ const cfg = { apis: [] }
16
+ let current = null
17
+ // 去掉 UTF-8 BOM(Windows 记事本默认会加),否则首个 key 解析失败
18
+ const text = readFileSync(join(dir, 'api.cfg'), 'utf8').replace(/^\uFEFF/, '')
19
+ for (const raw of text.split(/\r?\n/)) {
20
+ const line = raw.trim()
21
+ if (!line || line.startsWith('#')) continue
22
+ const m = line.match(/^([\w-]+)\s*=\s*(.*)$/)
23
+ if (m) {
24
+ current = m[1]
25
+ if (m[2]) cfg[current] = current === 'apis' ? m[2].split(',').map((s) => s.trim()).filter(Boolean) : m[2]
26
+ continue
27
+ }
28
+ // 无 = 的行归属上一个 key(apis 的逐行列举形式)
29
+ if (current === 'apis') cfg.apis.push(line)
30
+ }
31
+ return cfg
17
32
  } catch {
18
- return []
33
+ return empty
19
34
  }
20
35
  }
21
36
 
@@ -23,7 +38,7 @@ function loadApisTxt(dir) {
23
38
  function loadApiDoc(dir) {
24
39
  if (!dir) return ''
25
40
  try {
26
- return readFileSync(join(dir, 'api_doc.md'), 'utf8')
41
+ return readFileSync(join(dir, 'api_doc.md'), 'utf8').replace(/^\uFEFF/, '')
27
42
  } catch {
28
43
  return ''
29
44
  }
@@ -62,7 +77,7 @@ export function syncClientNs(dir, ns) {
62
77
  * @param {string} options.ns settings 命名空间,须与前端卡片的 NS 一致
63
78
  * @param {string} [options.toolPrefix] 工具名前缀默认值,生成 {prefix}_api_doc / {prefix}_api_request,可被 config 覆盖
64
79
  * @param {string} [options.domain] 领域名默认值,用于拼接工具描述 prompt,可被 config 覆盖
65
- * @param {string} [options.apiDir] apis.txt 与 api_doc.md 所在目录默认值,可被 config 覆盖
80
+ * @param {string} [options.apiDir] api.cfg 与 api_doc.md 所在目录默认值,可被 config 覆盖
66
81
  * @param {string} [options.defaultBaseUrl] 接口服务前缀默认值(域名 + nginx 前缀 + 网关路由段)
67
82
  * @param {string[]} [options.defaultEndpoints] yml 之外的额外默认接口
68
83
  */
@@ -74,12 +89,14 @@ export function defineApiPlugin(options) {
74
89
  domain: Schema.string().default(domain),
75
90
  // 工具名前缀:生成 {prefix}_api_doc / {prefix}_api_request
76
91
  toolPrefix: Schema.string().default(toolPrefix),
77
- // apis.txt 与 api_doc.md 所在目录(留空则只用 endpoints)
92
+ // api.cfg 与 api_doc.md 所在目录(留空则只用 endpoints)
78
93
  apiDir: Schema.string().default(apiDir),
79
94
  endpoints: Schema.array(Schema.string()).default([]),
80
95
  // 接口服务前缀:域名 + nginx 前缀 + 网关路由段
81
96
  // (接口路径本身已含服务前缀,最终形如 /prod-api/xxx/users/detail)
82
97
  baseUrl: Schema.string().default(defaultBaseUrl),
98
+ // 内部字段:客户端「加载API」按钮改写它触发服务端重扫目录
99
+ scanToken: Schema.string().default(''),
83
100
  })
84
101
 
85
102
  // 依赖部署挂载的设置服务与工具服务
@@ -89,24 +106,54 @@ export function defineApiPlugin(options) {
89
106
  // config 覆盖优先,回退工厂默认值(兼容旧用户层保存值缺字段的情况)
90
107
  const prefix = config.toolPrefix || toolPrefix
91
108
  const domainName = config.domain || domain
92
- const docDir = config.apiDir || apiDir
93
-
94
- // 合并 apis.txt yml 配置作为 base 层默认值(去重)
95
- const endpoints = [...new Set([...loadApisTxt(docDir), ...defaultEndpoints, ...(config.endpoints ?? [])])]
109
+ // 部署侧静态接口(yml config.endpoints + 工厂默认),与 api.cfg 动态读取分离,
110
+ // 避免「加载API」重扫时把启动时的旧 api.cfg 列表拼回去(导致删掉的接口仍可请求)
111
+ const ymlApiDir = config.apiDir || apiDir
112
+ const staticEndpoints = [...new Set([...defaultEndpoints, ...(config.endpoints ?? [])])]
113
+ // 启动时以当前目录 api.cfg 为权威范围,∪ 静态接口作为 base
114
+ const bootEndpoints = [...new Set([...loadApiCfg(ymlApiDir).apis, ...staticEndpoints])]
96
115
 
97
116
  // 以组合配置为 base 层注册 settings 命名空间,卸载插件时注册随 fiber 一并清理
98
- const scope = ctx.settings.register(ns, Config, { base: { ...config, endpoints } })
117
+ const scope = ctx.settings.register(ns, Config, { base: { ...config, endpoints: bootEndpoints } })
118
+
119
+ // 当前生效目录:用户层保存值优先(重启后仍生效),回退部署侧配置
120
+ let docDir = scope.get()?.apiDir || ymlApiDir
99
121
 
100
- // 每次已提交变更后收到通知
122
+ // 文档按当前生效目录读取;用户层改写 apiDir 后随 watch 刷新
123
+ let docSections = splitDocSections(loadApiDoc(docDir))
124
+
125
+ // 每次已提交变更后收到通知;「加载API」以 scanToken 变化标记,触发目录重扫
126
+ let lastScanToken = scope.get()?.scanToken ?? ''
101
127
  ctx.effect(
102
- () => scope.watch(() => {
128
+ () => {
129
+ console.log(`[${ns}] watch registering`)
130
+ return scope.watch((next) => {
131
+ try {
132
+ console.log(`[${ns}] watch fired; apiDir=${next.apiDir}, scanToken=${next.scanToken}`)
133
+ const dir = next.apiDir || apiDir
134
+ // 目录变化或显式重扫时重读文档
135
+ if (dir !== docDir || next.scanToken !== lastScanToken) {
136
+ docDir = dir
137
+ docSections = splitDocSections(loadApiDoc(dir))
138
+ }
139
+ if (next.scanToken !== lastScanToken) {
140
+ lastScanToken = next.scanToken
141
+ // 以最新 api.cfg 为权威范围 ∪ 静态接口,整体接管用户层 endpoints(去重;值未变时不提交,避免循环)
142
+ // 用最新读取结果而非启动时的快照,这样从 api.cfg 删掉的接口重扫后即失效
143
+ const merged = [...new Set([...loadApiCfg(dir).apis, ...staticEndpoints])]
144
+ const cur = Array.isArray(next.endpoints) ? next.endpoints : []
145
+ if (merged.join('\n') !== cur.join('\n')) scope.update({ endpoints: merged })
146
+ }
103
147
  console.log(`${ns}: settings updated`)
104
- }),
148
+ } catch (err) {
149
+ console.error(`[${ns}] watch handler error:`, err)
150
+ }
151
+ })
152
+ },
105
153
  `${ns}: settings watch`,
106
- )
154
+ )
107
155
 
108
156
  // ---------- agent 工具 ----------
109
- const docSections = splitDocSections(loadApiDoc(docDir))
110
157
 
111
158
  // 当前生效的接口列表(用户层保存值优先,回退默认值)
112
159
  function currentEndpoints() {
@@ -114,7 +161,7 @@ export function defineApiPlugin(options) {
114
161
  const value = scope.get()
115
162
  if (Array.isArray(value?.endpoints) && value.endpoints.length) return value.endpoints
116
163
  } catch { /* scope.get 不可用时回退 */ }
117
- return endpoints
164
+ return bootEndpoints
118
165
  }
119
166
 
120
167
  // 工具 1:查接口文档,让模型了解每个接口的参数与响应(prompt 按领域名拼接)
@@ -158,7 +205,12 @@ export function defineApiPlugin(options) {
158
205
  return `接口 ${norm} 不在已配置列表中。可用接口:\n` + list.join('\n')
159
206
  }
160
207
 
161
- const url = new URL(config.baseUrl.replace(/\/+$/, '') + matched)
208
+ // 服务前缀:api.cfg 优先(改文件即生效,无需重启),回退部署配置
209
+ const base = loadApiCfg(docDir).baseUrl || scope.get()?.baseUrl || ''
210
+ if (!base) {
211
+ return `未配置接口服务前缀 baseUrl。请在文档目录 ${docDir || '(未配置)'} 下新建 api.cfg 写入 baseUrl=http://... ,或在部署配置中设置。`
212
+ }
213
+ const url = new URL(base.replace(/\/+$/, '') + matched)
162
214
  for (const [k, v] of Object.entries(args.query ?? {})) {
163
215
  url.searchParams.set(k, String(v))
164
216
  }
@@ -178,7 +230,7 @@ export function defineApiPlugin(options) {
178
230
  },
179
231
  })
180
232
 
181
- console.log(`[${ns}] loaded; endpoints=${endpoints.length}, doc sections=${docSections.length}`)
233
+ console.log(`[${ns}] loaded; endpoints=${bootEndpoints.length}, doc sections=${docSections.length}`)
182
234
  }
183
235
 
184
236
  return { Config, inject, apply }
package/client.js CHANGED
@@ -34,6 +34,8 @@ window.__ModuleLoader__.load({
34
34
  .apis-save{background:var(--dsw-alias-label-primary);color:var(--dsw-alias-bg-layer-3)}
35
35
  .apis-row{display:flex;align-items:center;gap:8px}
36
36
  .apis-rowLabel{width:44px;flex-shrink:0;font-size:13px;color:var(--dsw-alias-label-primary)}
37
+ .apis-rowLabelWide{width:auto;white-space:nowrap}
38
+ .apis-hr{width:100%;height:0;border:0;border-top:.5px solid var(--dsw-alias-border-l2);margin:0}
37
39
  .apis-input{box-sizing:border-box;flex:1;min-width:0;font:inherit;color:var(--dsw-alias-label-primary);background:transparent;border:.5px solid var(--dsw-alias-border-l4);border-radius:8px;padding:6px 10px}
38
40
  .apis-remove{appearance:none;font:inherit;font-size:13px;cursor:pointer;background:none;border:none;border-radius:8px;padding:5px 10px;color:var(--dsw-alias-label-tertiary)}
39
41
  .apis-remove:hover:not(:disabled){color:var(--dsw-alias-label-primary)}
@@ -80,25 +82,49 @@ window.__ModuleLoader__.load({
80
82
  function ApisCard(props) {
81
83
  const state = props.useApisCard((s) => s);
82
84
  const [draft, setDraft] = react.useState(null); // string[],null 表示未编辑
85
+ const [dirDraft, setDirDraft] = react.useState(null); // 文档目录,null 表示未编辑
83
86
  const [saving, setSaving] = react.useState(false);
87
+ const [loading, setLoading] = react.useState(false); // 加载API 扫描中
84
88
  const [saved, setSaved] = react.useState(false); // 保存成功后短暂提示
89
+ const [loaded, setLoaded] = react.useState(false); // 加载API 完成后短暂提示
85
90
  const [open, setOpen] = react.useState(false); // 折叠态只显示标题行
86
91
  const ready = state.status === "ready" && state.writable;
87
92
 
88
93
  /** 当前展示的接口列表:编辑中取 draft,否则取已保存值 */
89
94
  const list = draft ?? (state.value?.endpoints ?? []).map(String);
90
95
  const dirty = draft !== null;
96
+ /** 当前展示的文档目录:编辑中取 dirDraft,否则取已保存值 */
97
+ const savedDir = state.value?.apiDir ?? "";
98
+ const shownDir = dirDraft ?? savedDir;
99
+ const dirDirty = dirDraft !== null && dirDraft !== savedDir;
91
100
 
92
101
  async function save() {
93
- if (saving || !ready || !dirty) return;
102
+ if (saving || !ready || (!dirty && !dirDirty)) return;
94
103
  setSaving(true);
95
- await scope.set("endpoints", draft);
104
+ if (dirDirty) await scope.set("apiDir", dirDraft); // 只存配置,扫描交给「加载API」
105
+ if (dirty) await scope.set("endpoints", draft);
96
106
  setSaving(false);
97
107
  setDraft(null);
108
+ setDirDraft(null);
98
109
  setSaved(true); // 显示「已保存」提示,2 秒后消失
99
110
  setTimeout(() => setSaved(false), 2000);
100
111
  }
101
112
 
113
+ /** 加载API:把目录写入配置并翻转 scanToken,服务端 watch 重扫 apis.txt 后回写接口列表 */
114
+ async function loadApis() {
115
+ if (loading || !ready || !shownDir.trim()) return;
116
+ setLoading(true);
117
+ try {
118
+ if (dirDirty) await scope.set("apiDir", shownDir.trim());
119
+ await scope.set("scanToken", String(Date.now()));
120
+ setDirDraft(null);
121
+ setLoaded(true);
122
+ setTimeout(() => setLoaded(false), 2000);
123
+ } finally {
124
+ setLoading(false);
125
+ }
126
+ }
127
+
102
128
  // 单行:接口N 标签 + 输入框 + 删除按钮
103
129
  const row = (v, i) => react_jsx_runtime.jsxs("div", {
104
130
  className: "apis-row",
@@ -135,8 +161,25 @@ window.__ModuleLoader__.load({
135
161
  react_jsx_runtime.jsx(Chevron, { open }),
136
162
  ],
137
163
  }),
138
- // 展开体:接口行列表 + 底部操作条(与原生一致:分隔线 + 右对齐按钮)
164
+ // 展开体:API配置目录行 + 分隔线 + 接口行列表 + 底部操作条
139
165
  open && react_jsx_runtime.jsxs("div", { className: "apis-body", children: [
166
+ // API配置:文档目录(apis.txt / api_doc.md 所在目录)+ 加载API 重扫按钮
167
+ react_jsx_runtime.jsxs("div", { className: "apis-row", children: [
168
+ react_jsx_runtime.jsx("div", { className: "apis-rowLabel apis-rowLabelWide", children: "API配置" }),
169
+ react_jsx_runtime.jsx("input", {
170
+ value: shownDir, disabled: !ready || saving || loading,
171
+ onChange: (e) => setDirDraft(e.target.value),
172
+ placeholder: "api.cfg / api_doc.md 所在目录",
173
+ className: "apis-input",
174
+ }),
175
+ react_jsx_runtime.jsx("button", {
176
+ type: "button", disabled: !ready || saving || loading || !shownDir.trim(),
177
+ onClick: loadApis,
178
+ className: "apis-btn apis-discard", children: loading ? "扫描中…" : "加载API",
179
+ }),
180
+ loaded && react_jsx_runtime.jsx("span", { className: "apis-saved", children: "已加载" }),
181
+ ] }),
182
+ react_jsx_runtime.jsx("hr", { className: "apis-hr" }),
140
183
  !state.writable && react_jsx_runtime.jsx("span", { className: "apis-description", children: "只读" }),
141
184
  list.map(row),
142
185
  react_jsx_runtime.jsx("button", {
@@ -148,12 +191,12 @@ window.__ModuleLoader__.load({
148
191
  // 保存成功后的短暂提示(占按钮行左侧)
149
192
  react_jsx_runtime.jsx("span", { className: "apis-saved", children: saved ? "已保存" : "" }),
150
193
  react_jsx_runtime.jsx("button", {
151
- type: "button", disabled: !dirty || saving,
152
- onClick: () => setDraft(null),
153
- className: "apis-btn apis-discard", children: "放弃修改",
194
+ type: "button", disabled: (!dirty && !dirDirty) || saving,
195
+ onClick: () => { setDraft(null); setDirDraft(null); },
196
+ className: "apis-btn apis-discard", children: "放弃修改",
154
197
  }),
155
198
  react_jsx_runtime.jsx("button", {
156
- type: "button", disabled: !ready || !dirty || saving,
199
+ type: "button", disabled: !ready || (!dirty && !dirDirty) || saving,
157
200
  onClick: save, children: saving ? "保存中…" : "保存",
158
201
  className: "apis-btn apis-save",
159
202
  }),
package/cordis.patch.yml CHANGED
@@ -7,9 +7,10 @@
7
7
  domain: ""
8
8
  # 工具名前缀:生成 {prefix}_api_doc / {prefix}_api_request
9
9
  toolPrefix: api
10
- # apis.txt 与 api_doc.md 所在目录(可选,留空则只用 endpoints)
10
+ # api.cfg 与 api_doc.md 所在目录(可选,留空则只用 endpoints)
11
11
  apiDir: ""
12
12
  # 默认接口列表
13
13
  endpoints: []
14
14
  # 接口服务前缀:域名 + nginx 前缀 + 网关路由段
15
+ # (也可在 apiDir 下 api.cfg 写 baseUrl=... 覆盖,改文件即生效无需重启)
15
16
  baseUrl: ""
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-apis-plugin",
3
- "version": "0.1.1",
3
+ "version": "0.1.3",
4
4
  "description": "通用接口管理插件:把任意一套 HTTP 接口注册成 agent 可用的文档查询与请求双工具",
5
5
  "type": "module",
6
6
  "license": "MIT",