@bolloon/bolloon-agent 0.3.23 → 0.3.25

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.
@@ -0,0 +1,193 @@
1
+ /**
2
+ * skill-writer.ts — skill 的创建 / 更新 / 删除 (2026-08-02)
3
+ *
4
+ * 背景: skill-loader.ts 只读 SKILL.md, 没有写路径 → agent 无法从成功经验沉淀技能,
5
+ * "越用越聪明" 的闭环缺失. 本模块补齐写侧:
6
+ *
7
+ * - createSkill(name, description, body, opts) → 写 ~/.bolloon/skills/<name>/SKILL.md
8
+ * - updateSkill(name, patch) → 更新已有 SKILL.md (description / body 追加或替换)
9
+ * - listSkillCandidates(dir) → 扫描 run-end 候选沉淀目录
10
+ * - loadSkillsFromPaths (复用 skill-loader)
11
+ *
12
+ * 安全:
13
+ * - 只写 ~/.bolloon/skills/ (全局) 和 <cwd>/.bolloon/skills/ (项目), 不碰其他路径
14
+ * - 目录名 sanitize: 只允许 [a-z0-9_-], 防路径穿越
15
+ * - 大小上限 50KB, 防 LLM 写爆
16
+ */
17
+ import * as fs from 'fs/promises';
18
+ import * as os from 'os';
19
+ import * as path from 'path';
20
+ /** 全局用户级 skills 目录 */
21
+ export function getUserSkillsDir(home = os.homedir()) {
22
+ return path.join(home, '.bolloon', 'skills');
23
+ }
24
+ /** 项目级 skills 目录 */
25
+ export function getProjectSkillsDir(cwd = process.cwd()) {
26
+ return path.join(cwd, '.bolloon', 'skills');
27
+ }
28
+ /** skill 名 sanitize: 只允许 [a-z0-9_-], 长度 ≤ 64, 去掉首尾连字符 */
29
+ export function sanitizeSkillName(name) {
30
+ return name.toLowerCase().replace(/[^a-z0-9_-]/g, '-').replace(/-+/g, '-').replace(/^-+|-+$/g, '').slice(0, 64);
31
+ }
32
+ /**
33
+ * 创建或覆盖一个 skill
34
+ * @param name skill 名 (会 sanitize)
35
+ * @param description 一句话描述
36
+ * @param body Markdown 正文 (步骤 / 命令 / 注意事项)
37
+ */
38
+ export async function createSkill(name, description, body, opts = {}) {
39
+ const safeName = sanitizeSkillName(name);
40
+ if (!safeName)
41
+ return { ok: false, path: '', error: 'skill 名非法 (sanitize 后为空)' };
42
+ if (body.length > 50_000)
43
+ return { ok: false, path: '', error: `正文过长 (${body.length} > 50000 字节)` };
44
+ const dir = opts.scope === 'project' ? getProjectSkillsDir() : getUserSkillsDir();
45
+ const skillDir = path.join(dir, safeName);
46
+ const file = path.join(skillDir, 'SKILL.md');
47
+ // 覆盖保护
48
+ if (opts.overwrite === false) {
49
+ try {
50
+ await fs.access(file);
51
+ return { ok: false, path: file, error: `skill '${safeName}' 已存在 (overwrite=false)` };
52
+ }
53
+ catch { /* 不存在, 可写 */ }
54
+ }
55
+ const status = opts.status || 'active';
56
+ const triggersBlock = opts.triggers && opts.triggers.length > 0
57
+ ? `triggers:\n${opts.triggers.map(t => ` - "${t.replace(/"/g, "'")}"`).join('\n')}\n`
58
+ : '';
59
+ const frontmatter = `---\nname: ${safeName}\ndescription: ${String(description || '').replace(/\n/g, ' ').slice(0, 200)}\nstatus: ${status}\n${triggersBlock}---\n`;
60
+ const content = frontmatter + '\n' + body.trim() + '\n';
61
+ try {
62
+ await fs.mkdir(skillDir, { recursive: true });
63
+ await fs.writeFile(file, content, 'utf-8');
64
+ return { ok: true, path: file };
65
+ }
66
+ catch (e) {
67
+ return { ok: false, path: file, error: `写入失败: ${e?.message || String(e)}` };
68
+ }
69
+ }
70
+ /**
71
+ * 更新已有 skill. 找不到则报错 (不自动创建).
72
+ */
73
+ export async function updateSkill(name, opts) {
74
+ const safeName = sanitizeSkillName(name);
75
+ if (!safeName)
76
+ return { ok: false, path: '', error: 'skill 名非法' };
77
+ // 先找现有文件 (用户级 + 项目级)
78
+ const candidates = [
79
+ path.join(getUserSkillsDir(), safeName, 'SKILL.md'),
80
+ path.join(getProjectSkillsDir(), safeName, 'SKILL.md'),
81
+ ];
82
+ let file = '';
83
+ for (const c of candidates) {
84
+ try {
85
+ await fs.access(c);
86
+ file = c;
87
+ break;
88
+ }
89
+ catch { /* 不存在 */ }
90
+ }
91
+ if (!file)
92
+ return { ok: false, path: '', error: `skill '${safeName}' 不存在, 用 create_skill 创建` };
93
+ try {
94
+ let raw = await fs.readFile(file, 'utf-8');
95
+ let body = raw.replace(/^---[\s\S]*?---\n?/, '').trim(); // 剥 frontmatter
96
+ if (opts.body !== undefined) {
97
+ body = opts.body.trim();
98
+ }
99
+ else if (opts.appendBody) {
100
+ body = (body + '\n\n' + opts.appendBody.trim()).slice(0, 50_000);
101
+ }
102
+ // 重建 frontmatter
103
+ const { parseSkillFile } = await import('./skill-loader.js');
104
+ const meta = await parseSkillFile(file);
105
+ const fm = meta?.frontmatter || {};
106
+ const fmLines = [];
107
+ fmLines.push(`name: ${safeName}`);
108
+ fmLines.push(`description: ${String(opts.description ?? meta?.description ?? '').replace(/\n/g, ' ').slice(0, 200)}`);
109
+ fmLines.push(`status: ${opts.status ?? meta?.status ?? 'active'}`);
110
+ const triggers = opts.triggers ?? meta?.triggers ?? [];
111
+ if (triggers.length > 0) {
112
+ fmLines.push('triggers:');
113
+ for (const t of triggers)
114
+ fmLines.push(` - "${String(t).replace(/"/g, "'")}"`);
115
+ }
116
+ const content = '---\n' + fmLines.join('\n') + '\n---\n\n' + body + '\n';
117
+ await fs.writeFile(file, content, 'utf-8');
118
+ return { ok: true, path: file };
119
+ }
120
+ catch (e) {
121
+ return { ok: false, path: file, error: `更新失败: ${e?.message || String(e)}` };
122
+ }
123
+ }
124
+ /** 删除 skill (返回删除的文件路径) */
125
+ export async function deleteSkill(name) {
126
+ const safeName = sanitizeSkillName(name);
127
+ if (!safeName)
128
+ return { ok: false, path: '', error: 'skill 名非法' };
129
+ const candidates = [
130
+ path.join(getUserSkillsDir(), safeName),
131
+ path.join(getProjectSkillsDir(), safeName),
132
+ ];
133
+ for (const c of candidates) {
134
+ try {
135
+ await fs.rm(c, { recursive: true, force: true });
136
+ return { ok: true, path: c };
137
+ }
138
+ catch { /* 尝试下一个 */ }
139
+ }
140
+ return { ok: false, path: '', error: `skill '${safeName}' 不存在` };
141
+ }
142
+ export function getCandidateDir(home = os.homedir()) {
143
+ return path.join(home, '.bolloon', 'skill-candidates');
144
+ }
145
+ export async function writeSkillCandidate(c) {
146
+ const dir = getCandidateDir();
147
+ await fs.mkdir(dir, { recursive: true });
148
+ const safeName = sanitizeSkillName(c.name);
149
+ const file = path.join(dir, `${safeName}-${Date.now()}.json`);
150
+ await fs.writeFile(file, JSON.stringify(c, null, 2), 'utf-8');
151
+ return file;
152
+ }
153
+ export async function listSkillCandidates(home = os.homedir()) {
154
+ const dir = getCandidateDir(home);
155
+ let entries;
156
+ try {
157
+ entries = await fs.readdir(dir);
158
+ }
159
+ catch {
160
+ return [];
161
+ }
162
+ const out = [];
163
+ for (const f of entries.filter(f => f.endsWith('.json')).sort().slice(-50)) {
164
+ try {
165
+ const raw = await fs.readFile(path.join(dir, f), 'utf-8');
166
+ const c = JSON.parse(raw);
167
+ if (c.name && c.body)
168
+ out.push(c);
169
+ }
170
+ catch { /* 坏文件跳过 */ }
171
+ }
172
+ return out;
173
+ }
174
+ /** 把候选转正为正式 skill (可选: 转正后删除候选文件) */
175
+ export async function promoteCandidate(name, opts = {}, home = os.homedir()) {
176
+ const candidates = await listSkillCandidates(home);
177
+ const c = candidates.find(x => x.name === name || sanitizeSkillName(x.name) === sanitizeSkillName(name));
178
+ if (!c)
179
+ return { ok: false, path: '', error: `候选 '${name}' 不存在` };
180
+ const r = await createSkill(c.name, c.description, c.body, opts);
181
+ if (r.ok) {
182
+ // 清理已转正的候选文件
183
+ try {
184
+ const dir = getCandidateDir(home);
185
+ for (const f of (await fs.readdir(dir))) {
186
+ if (f.startsWith(sanitizeSkillName(c.name) + '-'))
187
+ await fs.rm(path.join(dir, f), { force: true });
188
+ }
189
+ }
190
+ catch { /* 清理失败不阻塞 */ }
191
+ }
192
+ return r;
193
+ }
@@ -128,6 +128,58 @@ export class WorkflowPivotLoop {
128
128
  this.registerTool(tool);
129
129
  }
130
130
  }
131
+ /**
132
+ * 2026-08-02: 把 this.tools Map 转成 OpenAI 原生 tools 格式 (含参数 schema).
133
+ * pivot loop 之前只把工具描述塞 system prompt, LLM 靠文本 JSON 猜格式
134
+ * (deepseek 输出 {"name":"X","result":{...}} 编造结果), 从不真正执行工具.
135
+ * 传原生 tools + tool_choice auto → LLM 返回结构化 tool_calls.
136
+ */
137
+ buildOpenAITools() {
138
+ const out = [];
139
+ for (const [name, tool] of this.tools) {
140
+ const params = tool.parameters || {};
141
+ const properties = {};
142
+ const required = [];
143
+ for (const [pName, pDesc] of Object.entries(params)) {
144
+ properties[pName] = { type: 'string', description: String(pDesc) };
145
+ if (String(pDesc).includes('必填'))
146
+ required.push(pName);
147
+ }
148
+ out.push({
149
+ type: 'function',
150
+ function: {
151
+ name,
152
+ description: tool.description || name,
153
+ parameters: { type: 'object', properties, required },
154
+ },
155
+ });
156
+ }
157
+ return out;
158
+ }
159
+ /**
160
+ * 2026-08-02: 把 OpenAI 原生 tool_calls (结构化) 转成 ToolDefinition 数组.
161
+ * 优先于文本 JSON 解析 — LLM 返回 tool_calls 时直接执行, 不再猜格式.
162
+ */
163
+ nativeToolCallsToDefinitions(toolCalls) {
164
+ const out = [];
165
+ for (const tc of toolCalls || []) {
166
+ const fn = tc?.function;
167
+ if (!fn || !fn.name)
168
+ continue;
169
+ const name = fn.name;
170
+ if (!this.tools.has(name))
171
+ continue;
172
+ let args = {};
173
+ try {
174
+ const parsed = JSON.parse(fn.arguments || '{}');
175
+ if (parsed && typeof parsed === 'object')
176
+ args = parsed;
177
+ }
178
+ catch { /* args 保持空 */ }
179
+ out.push({ name, args: this.normalizeArgs(args), description: '', parameters: {} });
180
+ }
181
+ return out;
182
+ }
131
183
  /**
132
184
  * Execute the pivot loop
133
185
  */
@@ -202,9 +254,12 @@ export class WorkflowPivotLoop {
202
254
  try {
203
255
  // Call LLM
204
256
  const t0 = Date.now();
205
- const llmResponse = await llm.chat(context, headerForThisIter, signal);
206
- const reply = llmResponse.reply.trim();
207
- this.vlog(`[pivot] iter=${this.state.iteration} LLM took=${Date.now() - t0}ms reply=${reply.length} head=${reply.substring(0, 80).replace(/\n/g, ' ')}`);
257
+ // 2026-08-02: 传原生 OpenAI tools — deepseek 返回结构化 tool_calls,
258
+ // UI 才能显示真实的工具执行 step (之前靠文本 JSON 猜格式, LLM 编造 result)
259
+ const openAITools = this.buildOpenAITools();
260
+ const llmResponse = await llm.chat(context, headerForThisIter, signal, openAITools);
261
+ const reply = (llmResponse.reply || '').trim();
262
+ this.vlog(`[pivot] iter=${this.state.iteration} LLM took=${Date.now() - t0}ms reply=${reply.length} nativeToolCalls=${llmResponse.toolCalls?.length ?? 0} head=${reply.substring(0, 80).replace(/\n/g, ' ')}`);
208
263
  this.emit({ type: 'token', content: reply.substring(0, 100) });
209
264
  // 2026-07-06: 把完整 reply 推给前端 — 前端按需更新临时气泡
210
265
  // 之前只 emit token(100B 截断), 前端拿到 100B 看不清. 现在 emit preview 带完整 content.
@@ -248,7 +303,10 @@ export class WorkflowPivotLoop {
248
303
  return this.createResult(false, response, 'token_budget_exceeded');
249
304
  }
250
305
  // Check if this is a final response (no tool calls)
251
- const pendingTools = this.extractPendingToolUses(reply);
306
+ // 2026-08-02: 优先用 OpenAI 原生 tool_calls (结构化, LLM 不会编造 result),
307
+ // 没有才 fallback 到文本 JSON 解析 (extractPendingToolUses)
308
+ const nativeTools = this.nativeToolCallsToDefinitions(llmResponse.toolCalls || []);
309
+ const pendingTools = nativeTools.length > 0 ? nativeTools : this.extractPendingToolUses(reply);
252
310
  if (pendingTools.length === 0) {
253
311
  // 2026-07-06: LLM 显式 <final gen> 标记 — pivot 立即退出, 不再走 quality/iter 流程
254
312
  // 这个 marker 之前和 思考/ 同义被忽略, 害得 "你好" 类问题跑 11 iter 还不见停
@@ -558,29 +616,27 @@ export class WorkflowPivotLoop {
558
616
  // JSON parsing failed, ignore
559
617
  }
560
618
  // Pattern 4: Single JSON tool call format {"name": "tool_name", "arguments": {...}}
561
- try {
562
- const singleJsonRe = /\{\s*"name"\s*:\s*"(\w+)"\s*,\s*"arguments"\s*:\s*(\{[\s\S]*?\})\s*\}/g;
563
- let singleMatch;
564
- while ((singleMatch = singleJsonRe.exec(content)) !== null) {
565
- const name = singleMatch[1];
566
- if (pending.some(p => p.name === name))
567
- continue;
568
- if (!this.tools.has(name))
569
- continue;
570
- try {
571
- const args = JSON.parse(singleMatch[2]);
572
- if (args && typeof args === 'object') {
573
- const normalizedArgs = this.normalizeArgs(args);
574
- pending.push({ name, args: normalizedArgs, description: '', parameters: {} });
575
- }
576
- }
577
- catch {
578
- // JSON parsing failed, skip
619
+ // 2026-08-02 fix: 兼容 system prompt 教的 {"name":"X","input":{...}} 格式 —
620
+ // pi-sdk.ts 工具调用格式说明用 input 字段, 但解析器只认 arguments,
621
+ // 导致 LLM 输出 {"name":"read_document","input":{...}} 解析不到 → 工具永不执行。
622
+ const singleJsonRe = /\{\s*"name"\s*:\s*"(\w+)"\s*,\s*"(?:arguments|input)"\s*:\s*(\{[\s\S]*?\})\s*\}/g;
623
+ let singleMatch;
624
+ while ((singleMatch = singleJsonRe.exec(content)) !== null) {
625
+ const name = singleMatch[1];
626
+ if (pending.some(p => p.name === name))
627
+ continue;
628
+ if (!this.tools.has(name))
629
+ continue;
630
+ try {
631
+ const args = JSON.parse(singleMatch[2]);
632
+ if (args && typeof args === 'object') {
633
+ const normalizedArgs = this.normalizeArgs(args);
634
+ pending.push({ name, args: normalizedArgs, description: '', parameters: {} });
579
635
  }
580
636
  }
581
- }
582
- catch {
583
- // Pattern matching failed, ignore
637
+ catch {
638
+ // JSON parsing failed, skip
639
+ }
584
640
  }
585
641
  return pending;
586
642
  }
@@ -646,6 +702,11 @@ export class WorkflowPivotLoop {
646
702
  return this.messageHistory.map(m => {
647
703
  if (m.role === 'user')
648
704
  return `用户: ${m.content}`;
705
+ if (m.role === 'assistant' && m.toolCall && m.toolResult) {
706
+ // 2026-08-02 fix: 工具调用后的 assistant 消息带 toolCall/toolResult,
707
+ // 之前只输出 content (原生 tool_calls 时 content 为空) → LLM 永远看不到结果 → 无限重试同一工具
708
+ return `工具调用: ${m.toolCall.name}(${JSON.stringify(m.toolCall.args)})\n工具结果: ${JSON.stringify(m.toolResult)}`;
709
+ }
649
710
  if (m.role === 'assistant')
650
711
  return `助手: ${m.content}`;
651
712
  if (m.role === 'tool' && m.toolResult) {
@@ -0,0 +1,116 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ /**
3
+ * ink-app.tsx — Ink (React for CLI) 渲染入口
4
+ *
5
+ * 用 Yoga flexbox 布局实现: 内容置顶, 输入栏固定底部, 状态栏固定
6
+ */
7
+ import { useState, useEffect, useCallback, useRef } from 'react';
8
+ import { render, Box, Text, useInput, useApp } from 'ink';
9
+ import TextInput from 'ink-text-input';
10
+ import { brandArtLines, boxTop, boxRow, boxBottom, dispWidth } from './loading-tui.js';
11
+ // ─── 组件: Logo Box ──────────────────────────────────────────────────────────
12
+ const LogoBox = ({ width }) => {
13
+ const art = brandArtLines();
14
+ const mw = Math.max(40, ...art.map(l => dispWidth(l))) + 4;
15
+ const bw = Math.min(width - 2, mw);
16
+ const rows = [boxTop('Bolloon Agent', bw)];
17
+ for (const l of art)
18
+ rows.push(boxRow(l, bw, 'center'));
19
+ rows.push(boxBottom(bw));
20
+ return (_jsx(Box, { flexDirection: "column", children: rows.map((r, i) => _jsx(Text, { children: r }, i)) }));
21
+ };
22
+ // ─── 组件: 消息列表 ──────────────────────────────────────────────────────────
23
+ const Messages = ({ msgs }) => (_jsx(Box, { flexDirection: "column", flexGrow: 1, justifyContent: "flex-start", children: msgs.map((m, i) => {
24
+ const clean = m.replace(/\x1b\[[0-9;]*m/g, '');
25
+ return clean.trim() ? _jsx(Text, { children: clean ? m : '' }, i) : null;
26
+ }) }));
27
+ const InkApp = ({ onPrompt, initialStatus, getStatusUpdate, terminalW, terminalH }) => {
28
+ const [input, setInput] = useState('');
29
+ const [msgs, setMsgs] = useState([]);
30
+ const [status, setStatus] = useState(initialStatus);
31
+ const { exit } = useApp();
32
+ const [thinking, setThinking] = useState(false);
33
+ const thinkingIdx = useRef(0);
34
+ // 全局: 思考动画控制
35
+ useEffect(() => {
36
+ globalThis.__inkSetThinking = (v) => setThinking(v);
37
+ globalThis.__inkAppend = (line) => {
38
+ setMsgs(prev => [...prev, line]);
39
+ };
40
+ globalThis.__inkSetStatus = (s) => {
41
+ setStatus(s);
42
+ };
43
+ return () => {
44
+ delete globalThis.__inkAppend;
45
+ delete globalThis.__inkSetStatus;
46
+ delete globalThis.__inkSetThinking;
47
+ };
48
+ }, []);
49
+ const onSubmit = useCallback((value) => {
50
+ const trimmed = value.trim();
51
+ if (!trimmed)
52
+ return;
53
+ setInput('');
54
+ // 用户消息由 processInput 统一通过 appendLine(renderUserMessage) 显示
55
+ onPrompt(trimmed);
56
+ }, [onPrompt]);
57
+ useInput((_input, key) => {
58
+ if (key.ctrl && _input === 'c')
59
+ exit();
60
+ // TextInput handles actual input; useInput only for Ctrl+C
61
+ });
62
+ // 自动更新状态栏 (每秒)
63
+ useEffect(() => {
64
+ const timer = setInterval(() => {
65
+ const s = getStatusUpdate();
66
+ if (s)
67
+ setStatus(s);
68
+ }, 1000);
69
+ return () => clearInterval(timer);
70
+ }, [getStatusUpdate]);
71
+ // 思考动画 — kaomoji 旋转
72
+ const KAOMOJI = ['(`・ω・´)', '(´・_・`)', '(。•́︿•̀。)', 'ᕙ(▀̿̿Ĺ̯̿̿▀̿ ̿)ᕗ', '(◕‿◕)'];
73
+ useEffect(() => {
74
+ if (!thinking)
75
+ return;
76
+ const timer = setInterval(() => {
77
+ thinkingIdx.current = (thinkingIdx.current + 1) % KAOMOJI.length;
78
+ }, 600);
79
+ return () => clearInterval(timer);
80
+ }, [thinking]);
81
+ return (_jsxs(Box, { flexDirection: "column", height: "100%", children: [_jsxs(Box, { flexGrow: 1, flexDirection: "column", justifyContent: "flex-start", children: [_jsx(LogoBox, { width: terminalW }), _jsx(Messages, { msgs: msgs }), thinking && (_jsx(Box, { children: _jsxs(Text, { color: "yellow", children: [KAOMOJI[thinkingIdx.current], " \u601D\u8003\u4E2D..."] }) }))] }), _jsx(Box, { children: _jsx(Text, { bold: true, color: "white", children: '─'.repeat(terminalW) }) }), _jsx(Box, { children: _jsx(Text, { children: status }) }), _jsx(Box, { children: _jsx(Text, { bold: true, color: "white", children: '─'.repeat(terminalW) }) }), _jsxs(Box, { children: [_jsx(Text, { bold: true, color: "green", children: "\u276F " }), _jsx(TextInput, { value: input, onChange: setInput, onSubmit: onSubmit, placeholder: "\u8F93\u5165\u6D88\u606F..." })] }), _jsx(Box, { children: _jsx(Text, { bold: true, color: "white", children: '─'.repeat(terminalW) }) })] }));
82
+ };
83
+ // ─── 启动 ────────────────────────────────────────────────────────────────────
84
+ let _inkInstance = null;
85
+ export function startInk(onPrompt, initialStatus, getStatusUpdate) {
86
+ const tw = process.stdout.columns || 80;
87
+ const th = process.stdout.rows || 24;
88
+ _inkInstance = render(_jsx(InkApp, { onPrompt: onPrompt, initialStatus: initialStatus, getStatusUpdate: getStatusUpdate, terminalW: tw, terminalH: th }), {
89
+ stdout: process.stdout,
90
+ stdin: process.stdin,
91
+ exitOnCtrlC: false,
92
+ patchConsole: false, // 关键: 阻止 Ink 劫持 console.log
93
+ });
94
+ }
95
+ export function stopInk() {
96
+ if (_inkInstance) {
97
+ _inkInstance.unmount();
98
+ _inkInstance.clear();
99
+ _inkInstance = null;
100
+ }
101
+ }
102
+ export function inkAppendLine(line) {
103
+ const fn = globalThis.__inkAppend;
104
+ if (fn)
105
+ fn(line);
106
+ }
107
+ export function inkSetStatus(s) {
108
+ const fn = globalThis.__inkSetStatus;
109
+ if (fn)
110
+ fn(s);
111
+ }
112
+ export function inkSetThinking(v) {
113
+ const fn = globalThis.__inkSetThinking;
114
+ if (fn)
115
+ fn(v);
116
+ }
@@ -1,4 +1,4 @@
1
- import { buildClobClient } from './clobShared';
1
+ import { buildClobClient } from './clobShared.js';
2
2
  export async function cancelOrder(params) {
3
3
  if (!params.privateKey) {
4
4
  return { success: false, message: '取消订单需要提供钱包私钥 privateKey' };
@@ -1,4 +1,4 @@
1
- import { buildClobClient, fetchMarketMeta, resolveTokenId } from './clobShared';
1
+ import { buildClobClient, fetchMarketMeta, resolveTokenId } from './clobShared.js';
2
2
  export async function createOrder(params) {
3
3
  if (!params.privateKey) {
4
4
  return { success: false, message: '下单需要提供钱包私钥 privateKey (用于 EIP-712 订单签名)' };
@@ -1,4 +1,4 @@
1
- import { buildClobClient } from './clobShared';
1
+ import { buildClobClient } from './clobShared.js';
2
2
  export async function getOrders(params = {}) {
3
3
  if (!params.privateKey) {
4
4
  return { orders: [], message: '查询订单需要提供钱包私钥 privateKey' };