dsh-apis-plugin 0.1.0
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/README.md +1 -0
- package/api-tools.js +185 -0
- package/client.js +188 -0
- package/cordis.patch.yml +15 -0
- package/index.js +18 -0
- package/package.json +28 -0
package/README.md
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
# dsh-apis-plugin
|
package/api-tools.js
ADDED
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
// 可复用的「接口工具插件」工厂:把任意一套 HTTP 接口注册成 agent 的 {prefix}_api_doc / {prefix}_api_request 双工具
|
|
2
|
+
// 领域差异(settings 命名空间、工具名前缀、领域名、文档目录、baseUrl、默认接口列表)由调用方注入,
|
|
3
|
+
// 其中 domain / toolPrefix / apiDir 也可在部署侧 cordis.patch.yml 的 config 里覆盖;
|
|
4
|
+
// 工具描述 prompt 在这里按领域名拼接,实现一处逻辑多处复用
|
|
5
|
+
import { readFileSync, writeFileSync } from 'node:fs'
|
|
6
|
+
import { join } from 'node:path'
|
|
7
|
+
import Schema from '@deepseek-ai/schemastery'
|
|
8
|
+
|
|
9
|
+
// 读取目录下 apis.txt 作为默认接口列表;支持空行与 # 注释
|
|
10
|
+
function loadApisTxt(dir) {
|
|
11
|
+
if (!dir) return []
|
|
12
|
+
try {
|
|
13
|
+
return readFileSync(join(dir, 'apis.txt'), 'utf8')
|
|
14
|
+
.split(/\r?\n/)
|
|
15
|
+
.map((line) => line.trim())
|
|
16
|
+
.filter((line) => line && !line.startsWith('#'))
|
|
17
|
+
} catch {
|
|
18
|
+
return []
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// 读取目录下 api_doc.md 作为接口文档
|
|
23
|
+
function loadApiDoc(dir) {
|
|
24
|
+
if (!dir) return ''
|
|
25
|
+
try {
|
|
26
|
+
return readFileSync(join(dir, 'api_doc.md'), 'utf8')
|
|
27
|
+
} catch {
|
|
28
|
+
return ''
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// 把文档按 --- 分节,每节含一个接口的说明(兼容 CRLF 换行)
|
|
33
|
+
function splitDocSections(doc) {
|
|
34
|
+
return doc.split(/\r?\n-{3,}\r?\n/).map((s) => s.trim()).filter(Boolean)
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// 判断实际请求路径是否命中配置的接口(支持 {param} 模板段)
|
|
38
|
+
function matchEndpoint(path, configured) {
|
|
39
|
+
const pattern = new RegExp('^' + configured.replace(/\{[^}]+\}/g, '[^/]+') + '/?$')
|
|
40
|
+
return pattern.test(path)
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** 从 cordis.patch.yml 提取插件 id(insert 列表第一项),作为命名空间唯一来源 */
|
|
44
|
+
export function readPatchId(dir) {
|
|
45
|
+
const yml = readFileSync(join(dir, 'cordis.patch.yml'), 'utf8')
|
|
46
|
+
const m = yml.match(/^\s*-\s*id:\s*(\S+)\s*$/m)
|
|
47
|
+
if (!m) throw new Error('cordis.patch.yml 中未找到 insert 的 id')
|
|
48
|
+
return m[1]
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** 把 client.js 的 NS 字面量回写为 yml id(浏览器侧无法读 yml,只能靠此处同步) */
|
|
52
|
+
export function syncClientNs(dir, ns) {
|
|
53
|
+
const file = join(dir, 'client.js')
|
|
54
|
+
const code = readFileSync(file, 'utf8')
|
|
55
|
+
const updated = code.replace(/^(\s*const NS = ")[^"]*(";\s*$)/m, `$1${ns}$2`)
|
|
56
|
+
if (updated !== code) writeFileSync(file, updated)
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* 构造一个可被 cordis 直接加载的插件模块:{ Config, inject, apply }
|
|
61
|
+
* @param {object} options
|
|
62
|
+
* @param {string} options.ns settings 命名空间,须与前端卡片的 NS 一致
|
|
63
|
+
* @param {string} [options.toolPrefix] 工具名前缀默认值,生成 {prefix}_api_doc / {prefix}_api_request,可被 config 覆盖
|
|
64
|
+
* @param {string} [options.domain] 领域名默认值,用于拼接工具描述 prompt,可被 config 覆盖
|
|
65
|
+
* @param {string} [options.apiDir] apis.txt 与 api_doc.md 所在目录默认值,可被 config 覆盖
|
|
66
|
+
* @param {string} [options.defaultBaseUrl] 接口服务前缀默认值(域名 + nginx 前缀 + 网关路由段)
|
|
67
|
+
* @param {string[]} [options.defaultEndpoints] yml 之外的额外默认接口
|
|
68
|
+
*/
|
|
69
|
+
export function defineApiPlugin(options) {
|
|
70
|
+
const { ns, toolPrefix = 'api', domain = '', apiDir = '', defaultBaseUrl = '', defaultEndpoints = [] } = options
|
|
71
|
+
|
|
72
|
+
const Config = Schema.object({
|
|
73
|
+
// 领域名:用于拼接工具描述 prompt
|
|
74
|
+
domain: Schema.string().default(domain),
|
|
75
|
+
// 工具名前缀:生成 {prefix}_api_doc / {prefix}_api_request
|
|
76
|
+
toolPrefix: Schema.string().default(toolPrefix),
|
|
77
|
+
// apis.txt 与 api_doc.md 所在目录(留空则只用 endpoints)
|
|
78
|
+
apiDir: Schema.string().default(apiDir),
|
|
79
|
+
endpoints: Schema.array(Schema.string()).default([]),
|
|
80
|
+
// 接口服务前缀:域名 + nginx 前缀 + 网关路由段
|
|
81
|
+
// (接口路径本身已含服务前缀,最终形如 /prod-api/xxx/users/detail)
|
|
82
|
+
baseUrl: Schema.string().default(defaultBaseUrl),
|
|
83
|
+
})
|
|
84
|
+
|
|
85
|
+
// 依赖部署挂载的设置服务与工具服务
|
|
86
|
+
const inject = ['settings', 'tools']
|
|
87
|
+
|
|
88
|
+
function apply(ctx, config) {
|
|
89
|
+
// config 覆盖优先,回退工厂默认值(兼容旧用户层保存值缺字段的情况)
|
|
90
|
+
const prefix = config.toolPrefix || toolPrefix
|
|
91
|
+
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 ?? [])])]
|
|
96
|
+
|
|
97
|
+
// 以组合配置为 base 层注册 settings 命名空间,卸载插件时注册随 fiber 一并清理
|
|
98
|
+
const scope = ctx.settings.register(ns, Config, { base: { ...config, endpoints } })
|
|
99
|
+
|
|
100
|
+
// 每次已提交变更后收到通知
|
|
101
|
+
ctx.effect(
|
|
102
|
+
() => scope.watch(() => {
|
|
103
|
+
console.log(`${ns}: settings updated`)
|
|
104
|
+
}),
|
|
105
|
+
`${ns}: settings watch`,
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
// ---------- agent 工具 ----------
|
|
109
|
+
const docSections = splitDocSections(loadApiDoc(docDir))
|
|
110
|
+
|
|
111
|
+
// 当前生效的接口列表(用户层保存值优先,回退默认值)
|
|
112
|
+
function currentEndpoints() {
|
|
113
|
+
try {
|
|
114
|
+
const value = scope.get()
|
|
115
|
+
if (Array.isArray(value?.endpoints) && value.endpoints.length) return value.endpoints
|
|
116
|
+
} catch { /* scope.get 不可用时回退 */ }
|
|
117
|
+
return endpoints
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// 工具 1:查接口文档,让模型了解每个接口的参数与响应(prompt 按领域名拼接)
|
|
121
|
+
ctx.tools.register({
|
|
122
|
+
name: `${prefix}_api_doc`,
|
|
123
|
+
description: `查询${domainName}接口文档。传 path(如 /users/page)返回该接口的详细参数与响应说明;不传 path 返回通用说明和全部已配置接口目录。发起请求前先用它确认参数。`,
|
|
124
|
+
parameters: {
|
|
125
|
+
path: { type: 'string', description: '接口路径,如 /users/{id}' },
|
|
126
|
+
},
|
|
127
|
+
output: { schema: { type: 'string' }, render: (_args, value) => [{ type: 'text', text: value }] },
|
|
128
|
+
async execute(args) {
|
|
129
|
+
const list = currentEndpoints()
|
|
130
|
+
if (!args.path) {
|
|
131
|
+
return '【通用说明】\n' + (docSections[0] ?? '') + '\n\n【已配置接口目录】\n' + list.join('\n')
|
|
132
|
+
}
|
|
133
|
+
const norm = ('/' + args.path.trim().replace(/^\/+/, '')).split('?')[0]
|
|
134
|
+
const hit = docSections.filter((s) => s.includes('`' + norm + '`'))
|
|
135
|
+
if (hit.length) return hit.join('\n\n---\n\n')
|
|
136
|
+
return `文档中未找到 ${norm}。已配置接口目录:\n` + list.join('\n')
|
|
137
|
+
},
|
|
138
|
+
})
|
|
139
|
+
|
|
140
|
+
// 工具 2:真正调用接口拿数据(prompt 按领域名拼接)
|
|
141
|
+
ctx.tools.register({
|
|
142
|
+
name: `${prefix}_api_request`,
|
|
143
|
+
description: `调用${domainName}接口并返回 JSON 数据。path 必须是已配置的接口(支持 /orders/{id} 这类模板路径,模板段填实际值);GET 用 query 传参,POST 用 body 传 JSON。调用前建议先用 ${prefix}_api_doc 确认参数。`,
|
|
144
|
+
parameters: {
|
|
145
|
+
path: { type: 'string', required: true, description: '接口路径,如 /users/detail 或 /orders/123' },
|
|
146
|
+
method: { type: 'string', description: 'HTTP 方法,默认 GET' },
|
|
147
|
+
query: { type: 'json', description: 'query 参数对象,如 { "keyword": "abc" }' },
|
|
148
|
+
body: { type: 'json', description: 'POST 请求体(JSON 对象)' },
|
|
149
|
+
},
|
|
150
|
+
output: { schema: { type: 'string' }, render: (_args, value) => [{ type: 'text', text: value }] },
|
|
151
|
+
async execute(args) {
|
|
152
|
+
const list = currentEndpoints()
|
|
153
|
+
const method = (args.method ?? 'GET').toUpperCase()
|
|
154
|
+
const norm = ('/' + args.path.trim().replace(/^\/+/, '')).split('?')[0]
|
|
155
|
+
|
|
156
|
+
const matched = list.find((e) => matchEndpoint(norm, e))
|
|
157
|
+
if (!matched) {
|
|
158
|
+
return `接口 ${norm} 不在已配置列表中。可用接口:\n` + list.join('\n')
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
const url = new URL(config.baseUrl.replace(/\/+$/, '') + matched)
|
|
162
|
+
for (const [k, v] of Object.entries(args.query ?? {})) {
|
|
163
|
+
url.searchParams.set(k, String(v))
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
const response = await fetch(url, {
|
|
167
|
+
method,
|
|
168
|
+
headers: { Accept: 'application/json', ...(args.body != null ? { 'Content-Type': 'application/json' } : {}) },
|
|
169
|
+
...(method !== 'GET' && args.body != null ? { body: JSON.stringify(args.body) } : {}),
|
|
170
|
+
})
|
|
171
|
+
const text = await response.text()
|
|
172
|
+
|
|
173
|
+
if (!response.ok) {
|
|
174
|
+
return `HTTP ${response.status} ${response.statusText}\n` + text.slice(0, 4000)
|
|
175
|
+
}
|
|
176
|
+
// 非文件流接口直接返回 JSON 文本,超长截断
|
|
177
|
+
return text.length > 30000 ? text.slice(0, 30000) + '\n…(超长截断)' : text
|
|
178
|
+
},
|
|
179
|
+
})
|
|
180
|
+
|
|
181
|
+
console.log(`[${ns}] loaded; endpoints=${endpoints.length}, doc sections=${docSections.length}`)
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
return { Config, inject, apply }
|
|
185
|
+
}
|
package/client.js
ADDED
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
// 浏览器半侧:在「插件配置」标签页为 dsh-apis-plugin 命名空间注册一张可编辑卡片
|
|
2
|
+
// 样式与 DOM 结构对齐原生 PluginCard(li 卡片 + SVG 箭头 + 放弃/保存 footer)
|
|
3
|
+
window.__ModuleLoader__.load({
|
|
4
|
+
id: "dsh-apis-plugin",
|
|
5
|
+
factory: (require) => {
|
|
6
|
+
var module = { exports: {} };
|
|
7
|
+
var exports = module.exports;
|
|
8
|
+
let react_jsx_runtime = require("react/jsx-runtime");
|
|
9
|
+
let react = require("react");
|
|
10
|
+
|
|
11
|
+
/** 本卡片编辑的 settings 命名空间,由宿主侧 index.js 按 cordis.patch.yml 的 id 自动回写 */
|
|
12
|
+
const NS = "dsh-apis-plugin";
|
|
13
|
+
|
|
14
|
+
/** bind 后的命名空间 scope,写入经由它进行(apply 时赋值) */
|
|
15
|
+
let scope;
|
|
16
|
+
|
|
17
|
+
// 复刻原生 PluginCard.module.css 的样式(类名自持,不依赖其哈希类)
|
|
18
|
+
const css = `
|
|
19
|
+
.apis-card{border:.5px solid var(--dsw-alias-border-l4);background:var(--dsw-alias-bg-layer-3);border-radius:16px;list-style:none;transition:border-color .16s,background .16s}
|
|
20
|
+
.apis-card:hover{border-color:var(--dsw-alias-label-dimmed)}
|
|
21
|
+
.apis-card[data-open=true]{background:var(--dsw-alias-bg-layer-2);border-color:var(--dsw-alias-label-dimmed)}
|
|
22
|
+
.apis-header{appearance:none;width:100%;font:inherit;color:inherit;text-align:left;cursor:pointer;background:0 0;border:0;border-radius:12px;align-items:center;gap:12px;padding:14px 16px;display:flex}
|
|
23
|
+
.apis-headText{flex-direction:column;flex:1;gap:4px;min-width:0;display:flex}
|
|
24
|
+
.apis-name{color:var(--dsw-alias-label-primary);font-size:15px;font-weight:600;line-height:1.4}
|
|
25
|
+
.apis-description{color:var(--dsw-alias-label-tertiary);font-size:13px;line-height:1.5}
|
|
26
|
+
.apis-chevron{color:var(--dsw-alias-label-tertiary);flex:none;transition:transform .16s}
|
|
27
|
+
.apis-body{border-top:.5px solid var(--dsw-alias-border-l2);margin:0 16px;padding:12px 0 8px;display:grid;gap:10px}
|
|
28
|
+
.apis-footer{border-top:.5px solid var(--dsw-alias-border-l2);justify-content:flex-end;align-items:center;gap:8px;padding:12px 0 4px;display:flex}
|
|
29
|
+
.apis-saved{color:var(--dsw-alias-label-tertiary);margin-right:auto;font-size:12px}
|
|
30
|
+
.apis-btn{appearance:none;font:inherit;cursor:pointer;border:1px solid transparent;border-radius:8px;padding:5px 14px;font-size:13px;line-height:1.5}
|
|
31
|
+
.apis-btn:disabled{opacity:.4;cursor:default}
|
|
32
|
+
.apis-discard{border-color:var(--dsw-alias-border-l2);color:var(--dsw-alias-label-secondary);background:0 0}
|
|
33
|
+
.apis-discard:hover:not(:disabled){color:var(--dsw-alias-label-primary);border-color:var(--dsw-alias-label-dimmed)}
|
|
34
|
+
.apis-save{background:var(--dsw-alias-label-primary);color:var(--dsw-alias-bg-layer-3)}
|
|
35
|
+
.apis-row{display:flex;align-items:center;gap:8px}
|
|
36
|
+
.apis-rowLabel{width:44px;flex-shrink:0;font-size:13px;color:var(--dsw-alias-label-primary)}
|
|
37
|
+
.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
|
+
.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
|
+
.apis-remove:hover:not(:disabled){color:var(--dsw-alias-label-primary)}
|
|
40
|
+
.apis-add{appearance:none;font:inherit;font-size:13px;cursor:pointer;background:none;border:none;border-radius:8px;padding:2px 0;color:var(--dsw-alias-label-secondary);margin-right:auto}
|
|
41
|
+
.apis-add:hover:not(:disabled){color:var(--dsw-alias-label-primary)}
|
|
42
|
+
`;
|
|
43
|
+
if (typeof document !== "undefined" && document.querySelector("style[data-plugin-css=dsh-apis-plugin]") === null) {
|
|
44
|
+
const tag = document.createElement("style");
|
|
45
|
+
tag.dataset.pluginCss = "dsh-apis-plugin";
|
|
46
|
+
tag.textContent = css;
|
|
47
|
+
document.head.appendChild(tag);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** 与 slots 的 observableHook 约定兼容的极简 snapshot store(getSnapshot/subscribe) */
|
|
51
|
+
function createStore(initial) {
|
|
52
|
+
let state = initial;
|
|
53
|
+
const listeners = new Set();
|
|
54
|
+
return {
|
|
55
|
+
getSnapshot: () => state,
|
|
56
|
+
subscribe(listener) {
|
|
57
|
+
listeners.add(listener);
|
|
58
|
+
return () => listeners.delete(listener);
|
|
59
|
+
},
|
|
60
|
+
set(next) {
|
|
61
|
+
state = next;
|
|
62
|
+
listeners.forEach((l) => l());
|
|
63
|
+
},
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** 原生卡片同款的下拉箭头(14px 线性 chevron) */
|
|
68
|
+
function Chevron({ open }) {
|
|
69
|
+
return react_jsx_runtime.jsx("svg", {
|
|
70
|
+
width: 14, height: 14, viewBox: "0 0 14 14", fill: "none", "aria-hidden": true,
|
|
71
|
+
className: "apis-chevron",
|
|
72
|
+
style: { transform: open ? "rotate(180deg)" : "none" },
|
|
73
|
+
children: react_jsx_runtime.jsx("path", {
|
|
74
|
+
d: "M3.5 5.25 7 8.75l3.5-3.5",
|
|
75
|
+
stroke: "currentColor", strokeWidth: 1.2, strokeLinecap: "round", strokeLinejoin: "round",
|
|
76
|
+
}),
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function ApisCard(props) {
|
|
81
|
+
const state = props.useApisCard((s) => s);
|
|
82
|
+
const [draft, setDraft] = react.useState(null); // string[],null 表示未编辑
|
|
83
|
+
const [saving, setSaving] = react.useState(false);
|
|
84
|
+
const [saved, setSaved] = react.useState(false); // 保存成功后短暂提示
|
|
85
|
+
const [open, setOpen] = react.useState(false); // 折叠态只显示标题行
|
|
86
|
+
const ready = state.status === "ready" && state.writable;
|
|
87
|
+
|
|
88
|
+
/** 当前展示的接口列表:编辑中取 draft,否则取已保存值 */
|
|
89
|
+
const list = draft ?? (state.value?.endpoints ?? []).map(String);
|
|
90
|
+
const dirty = draft !== null;
|
|
91
|
+
|
|
92
|
+
async function save() {
|
|
93
|
+
if (saving || !ready || !dirty) return;
|
|
94
|
+
setSaving(true);
|
|
95
|
+
await scope.set("endpoints", draft);
|
|
96
|
+
setSaving(false);
|
|
97
|
+
setDraft(null);
|
|
98
|
+
setSaved(true); // 显示「已保存」提示,2 秒后消失
|
|
99
|
+
setTimeout(() => setSaved(false), 2000);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// 单行:接口N 标签 + 输入框 + 删除按钮
|
|
103
|
+
const row = (v, i) => react_jsx_runtime.jsxs("div", {
|
|
104
|
+
className: "apis-row",
|
|
105
|
+
children: [
|
|
106
|
+
react_jsx_runtime.jsx("div", { className: "apis-rowLabel", children: `接口${i + 1}` }),
|
|
107
|
+
react_jsx_runtime.jsx("input", {
|
|
108
|
+
value: v, disabled: !ready || saving,
|
|
109
|
+
onChange: (e) => setDraft(list.map((x, j) => (j === i ? e.target.value : x))),
|
|
110
|
+
className: "apis-input",
|
|
111
|
+
}),
|
|
112
|
+
react_jsx_runtime.jsx("button", {
|
|
113
|
+
type: "button", disabled: !ready || saving, "aria-label": `删除接口${i + 1}`,
|
|
114
|
+
onClick: () => setDraft(list.filter((_, j) => j !== i)),
|
|
115
|
+
className: "apis-remove", children: "✕",
|
|
116
|
+
}),
|
|
117
|
+
],
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
return react_jsx_runtime.jsxs("li", {
|
|
121
|
+
className: "apis-card",
|
|
122
|
+
"data-open": open,
|
|
123
|
+
children: [
|
|
124
|
+
// 折叠头:标题 + 描述 + 箭头,点击切换展开
|
|
125
|
+
react_jsx_runtime.jsxs("button", {
|
|
126
|
+
type: "button",
|
|
127
|
+
className: "apis-header",
|
|
128
|
+
"aria-expanded": open,
|
|
129
|
+
onClick: () => setOpen((v) => !v),
|
|
130
|
+
children: [
|
|
131
|
+
react_jsx_runtime.jsxs("div", { className: "apis-headText", children: [
|
|
132
|
+
react_jsx_runtime.jsx("span", { className: "apis-name", children: "通用接口管理插件" }),
|
|
133
|
+
react_jsx_runtime.jsx("span", { className: "apis-description", children: "接口列表配置" }),
|
|
134
|
+
] }),
|
|
135
|
+
react_jsx_runtime.jsx(Chevron, { open }),
|
|
136
|
+
],
|
|
137
|
+
}),
|
|
138
|
+
// 展开体:接口行列表 + 底部操作条(与原生一致:分隔线 + 右对齐按钮)
|
|
139
|
+
open && react_jsx_runtime.jsxs("div", { className: "apis-body", children: [
|
|
140
|
+
!state.writable && react_jsx_runtime.jsx("span", { className: "apis-description", children: "只读" }),
|
|
141
|
+
list.map(row),
|
|
142
|
+
react_jsx_runtime.jsx("button", {
|
|
143
|
+
type: "button", disabled: !ready || saving,
|
|
144
|
+
onClick: () => setDraft([...list, ""]),
|
|
145
|
+
className: "apis-add", children: "+ 添加接口",
|
|
146
|
+
}),
|
|
147
|
+
react_jsx_runtime.jsxs("div", { className: "apis-footer", children: [
|
|
148
|
+
// 保存成功后的短暂提示(占按钮行左侧)
|
|
149
|
+
react_jsx_runtime.jsx("span", { className: "apis-saved", children: saved ? "已保存" : "" }),
|
|
150
|
+
react_jsx_runtime.jsx("button", {
|
|
151
|
+
type: "button", disabled: !dirty || saving,
|
|
152
|
+
onClick: () => setDraft(null),
|
|
153
|
+
className: "apis-btn apis-discard", children: "放弃修改",
|
|
154
|
+
}),
|
|
155
|
+
react_jsx_runtime.jsx("button", {
|
|
156
|
+
type: "button", disabled: !ready || !dirty || saving,
|
|
157
|
+
onClick: save, children: saving ? "保存中…" : "保存",
|
|
158
|
+
className: "apis-btn apis-save",
|
|
159
|
+
}),
|
|
160
|
+
] }),
|
|
161
|
+
] }),
|
|
162
|
+
],
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/** 需要的浏览器侧服务 */
|
|
167
|
+
const inject = ["slots", "settingsScope"];
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* 以命名空间为 key 注册到 settings.plugin.item 槽位:
|
|
171
|
+
* 「插件配置」标签页会为每个被服务且被认领的命名空间渲染一张卡片
|
|
172
|
+
*/
|
|
173
|
+
function apply(ctx) {
|
|
174
|
+
scope = ctx.settingsScope.bind({ namespace: NS });
|
|
175
|
+
const store = createStore(scope.getSnapshot());
|
|
176
|
+
ctx.effect(() => scope.subscribe(() => store.set(scope.getSnapshot())));
|
|
177
|
+
ctx.effect(() => ctx.slots.inject("settings.plugin.item", () => ctx.slots.register({
|
|
178
|
+
name: "settings.plugin.item",
|
|
179
|
+
key: NS,
|
|
180
|
+
inject: () => ({ hooks: { apisCard: store } }),
|
|
181
|
+
}, ApisCard)));
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
exports.apply = apply;
|
|
185
|
+
exports.inject = inject;
|
|
186
|
+
return module.exports;
|
|
187
|
+
}
|
|
188
|
+
});
|
package/cordis.patch.yml
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
- insert:
|
|
2
|
+
- id: dsh-apis-plugin
|
|
3
|
+
# 注:商店发布形态用包名;本地开发调试时临时换回 file:// 直连源码
|
|
4
|
+
name: dsh-apis-plugin
|
|
5
|
+
config:
|
|
6
|
+
# 领域名:拼接工具描述 prompt
|
|
7
|
+
domain: ""
|
|
8
|
+
# 工具名前缀:生成 {prefix}_api_doc / {prefix}_api_request
|
|
9
|
+
toolPrefix: api
|
|
10
|
+
# apis.txt 与 api_doc.md 所在目录(可选,留空则只用 endpoints)
|
|
11
|
+
apiDir: ""
|
|
12
|
+
# 默认接口列表
|
|
13
|
+
endpoints: []
|
|
14
|
+
# 接口服务前缀:域名 + nginx 前缀 + 网关路由段
|
|
15
|
+
baseUrl: ""
|
package/index.js
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
// 通用接口管理插件:把任意一套 HTTP 接口注册成 agent 可用的文档查询与请求双工具
|
|
2
|
+
// 领域差异(工具前缀、领域名、文档目录、baseUrl、默认接口列表)全部由部署侧 cordis.patch.yml 的 config 注入;
|
|
3
|
+
// 通用逻辑(工具注册、prompt 拼接、接口匹配、settings 注册)在同目录 api-tools.js 的 defineApiPlugin
|
|
4
|
+
import { fileURLToPath } from 'node:url'
|
|
5
|
+
import { dirname } from 'node:path'
|
|
6
|
+
import { defineApiPlugin, readPatchId, syncClientNs } from './api-tools.js'
|
|
7
|
+
|
|
8
|
+
const here = dirname(fileURLToPath(import.meta.url))
|
|
9
|
+
|
|
10
|
+
// 命名空间以 cordis.patch.yml 的 id 为准,加载时自动同步到 client.js
|
|
11
|
+
const ns = readPatchId(here)
|
|
12
|
+
syncClientNs(here, ns)
|
|
13
|
+
|
|
14
|
+
const plugin = defineApiPlugin({ ns })
|
|
15
|
+
|
|
16
|
+
export const Config = plugin.Config
|
|
17
|
+
export const inject = plugin.inject
|
|
18
|
+
export const apply = plugin.apply
|
package/package.json
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "dsh-apis-plugin",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "通用接口管理插件:把任意一套 HTTP 接口注册成 agent 可用的文档查询与请求双工具",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "index.js",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": "./index.js",
|
|
9
|
+
"./client": "./client.js"
|
|
10
|
+
},
|
|
11
|
+
"files": [
|
|
12
|
+
"index.js",
|
|
13
|
+
"client.js",
|
|
14
|
+
"api-tools.js",
|
|
15
|
+
"cordis.patch.yml"
|
|
16
|
+
],
|
|
17
|
+
"dsh": {
|
|
18
|
+
"bundle": {
|
|
19
|
+
"patch": "./cordis.patch.yml"
|
|
20
|
+
},
|
|
21
|
+
"client": {
|
|
22
|
+
"platform": "web"
|
|
23
|
+
}
|
|
24
|
+
},
|
|
25
|
+
"dependencies": {
|
|
26
|
+
"@deepseek-ai/schemastery": "^3.18.2"
|
|
27
|
+
}
|
|
28
|
+
}
|