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 +76 -24
- package/client.js +50 -7
- package/cordis.patch.yml +2 -1
- package/package.json +1 -1
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
|
-
// 读取目录下
|
|
10
|
-
|
|
11
|
-
|
|
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
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
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]
|
|
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
|
-
//
|
|
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
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
const
|
|
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
|
-
() =>
|
|
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
|
|
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
|
-
|
|
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=${
|
|
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("
|
|
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
|
-
|
|
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
|
-
#
|
|
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: ""
|