galaxy-opc-plugin 0.1.1 → 0.2.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.
@@ -2,25 +2,29 @@
2
2
  * 星环OPC中心 — TypeBox 工具参数 Schema
3
3
  *
4
4
  * 使用 Type.Union + action 字符串字段,兼容所有 LLM Provider。
5
+ * 包含业务规则校验约束(字符串长度、金额范围、日期格式等)。
5
6
  */
6
7
 
7
8
  import { Type, type Static } from "@sinclair/typebox";
8
9
 
10
+ /** 日期格式 pattern: YYYY-MM-DD */
11
+ const DATE_PATTERN = "^\\d{4}-\\d{2}-\\d{2}$";
12
+
9
13
  // ── 公司管理 Schema ──────────────────────────────────────────
10
14
 
11
15
  const RegisterCompany = Type.Object({
12
16
  action: Type.Literal("register_company"),
13
- name: Type.String({ description: "公司名称" }),
14
- industry: Type.String({ description: "所属行业" }),
15
- owner_name: Type.String({ description: "创办人姓名" }),
16
- owner_contact: Type.Optional(Type.String({ description: "创办人联系方式(手机/邮箱)" })),
17
- registered_capital: Type.Optional(Type.Number({ description: "注册资本(元)" })),
18
- description: Type.Optional(Type.String({ description: "公司简介" })),
17
+ name: Type.String({ description: "公司名称", minLength: 2, maxLength: 100 }),
18
+ industry: Type.String({ description: "所属行业", minLength: 1, maxLength: 50 }),
19
+ owner_name: Type.String({ description: "创办人姓名", minLength: 1, maxLength: 50 }),
20
+ owner_contact: Type.Optional(Type.String({ description: "创办人联系方式(手机/邮箱)", maxLength: 200 })),
21
+ registered_capital: Type.Optional(Type.Number({ description: "注册资本(元)", minimum: 0 })),
22
+ description: Type.Optional(Type.String({ description: "公司简介", maxLength: 2000 })),
19
23
  });
20
24
 
21
25
  const GetCompany = Type.Object({
22
26
  action: Type.Literal("get_company"),
23
- company_id: Type.String({ description: "公司 ID" }),
27
+ company_id: Type.String({ description: "公司 ID", minLength: 1 }),
24
28
  });
25
29
 
26
30
  const ListCompanies = Type.Object({
@@ -32,21 +36,21 @@ const ListCompanies = Type.Object({
32
36
 
33
37
  const UpdateCompany = Type.Object({
34
38
  action: Type.Literal("update_company"),
35
- company_id: Type.String({ description: "公司 ID" }),
36
- name: Type.Optional(Type.String({ description: "新公司名称" })),
37
- industry: Type.Optional(Type.String({ description: "新行业" })),
38
- description: Type.Optional(Type.String({ description: "新简介" })),
39
- owner_contact: Type.Optional(Type.String({ description: "新联系方式" })),
39
+ company_id: Type.String({ description: "公司 ID", minLength: 1 }),
40
+ name: Type.Optional(Type.String({ description: "新公司名称", minLength: 2, maxLength: 100 })),
41
+ industry: Type.Optional(Type.String({ description: "新行业", minLength: 1, maxLength: 50 })),
42
+ description: Type.Optional(Type.String({ description: "新简介", maxLength: 2000 })),
43
+ owner_contact: Type.Optional(Type.String({ description: "新联系方式", maxLength: 200 })),
40
44
  });
41
45
 
42
46
  const ActivateCompany = Type.Object({
43
47
  action: Type.Literal("activate_company"),
44
- company_id: Type.String({ description: "公司 ID" }),
48
+ company_id: Type.String({ description: "公司 ID", minLength: 1 }),
45
49
  });
46
50
 
47
51
  const ChangeCompanyStatus = Type.Object({
48
52
  action: Type.Literal("change_company_status"),
49
- company_id: Type.String({ description: "公司 ID" }),
53
+ company_id: Type.String({ description: "公司 ID", minLength: 1 }),
50
54
  new_status: Type.String({ description: "目标状态: active/suspended/acquired/packaged/terminated" }),
51
55
  });
52
56
 
@@ -54,7 +58,7 @@ const ChangeCompanyStatus = Type.Object({
54
58
 
55
59
  const AddTransaction = Type.Object({
56
60
  action: Type.Literal("add_transaction"),
57
- company_id: Type.String({ description: "公司 ID" }),
61
+ company_id: Type.String({ description: "公司 ID", minLength: 1 }),
58
62
  type: Type.String({ description: "交易类型: income(收入) 或 expense(支出)" }),
59
63
  category: Type.Optional(
60
64
  Type.String({
@@ -62,62 +66,62 @@ const AddTransaction = Type.Object({
62
66
  "分类: service_income/product_income/investment_income/salary/rent/utilities/marketing/tax/supplies/other",
63
67
  }),
64
68
  ),
65
- amount: Type.Number({ description: "金额(元)" }),
66
- description: Type.Optional(Type.String({ description: "交易描述" })),
67
- counterparty: Type.Optional(Type.String({ description: "交易对方" })),
68
- transaction_date: Type.Optional(Type.String({ description: "交易日期 (YYYY-MM-DD),默认今天" })),
69
+ amount: Type.Number({ description: "金额(元)", minimum: 0 }),
70
+ description: Type.Optional(Type.String({ description: "交易描述", maxLength: 500 })),
71
+ counterparty: Type.Optional(Type.String({ description: "交易对方", maxLength: 200 })),
72
+ transaction_date: Type.Optional(Type.String({ description: "交易日期 (YYYY-MM-DD),默认今天", pattern: DATE_PATTERN })),
69
73
  });
70
74
 
71
75
  const ListTransactions = Type.Object({
72
76
  action: Type.Literal("list_transactions"),
73
- company_id: Type.String({ description: "公司 ID" }),
77
+ company_id: Type.String({ description: "公司 ID", minLength: 1 }),
74
78
  type: Type.Optional(Type.String({ description: "按类型筛选: income/expense" })),
75
- start_date: Type.Optional(Type.String({ description: "起始日期 (YYYY-MM-DD)" })),
76
- end_date: Type.Optional(Type.String({ description: "截止日期 (YYYY-MM-DD)" })),
77
- limit: Type.Optional(Type.Number({ description: "返回条数,默认 50" })),
79
+ start_date: Type.Optional(Type.String({ description: "起始日期 (YYYY-MM-DD)", pattern: DATE_PATTERN })),
80
+ end_date: Type.Optional(Type.String({ description: "截止日期 (YYYY-MM-DD)", pattern: DATE_PATTERN })),
81
+ limit: Type.Optional(Type.Number({ description: "返回条数,默认 50", minimum: 1, maximum: 1000 })),
78
82
  });
79
83
 
80
84
  const GetFinanceSummary = Type.Object({
81
85
  action: Type.Literal("finance_summary"),
82
- company_id: Type.String({ description: "公司 ID" }),
83
- start_date: Type.Optional(Type.String({ description: "起始日期 (YYYY-MM-DD)" })),
84
- end_date: Type.Optional(Type.String({ description: "截止日期 (YYYY-MM-DD)" })),
86
+ company_id: Type.String({ description: "公司 ID", minLength: 1 }),
87
+ start_date: Type.Optional(Type.String({ description: "起始日期 (YYYY-MM-DD)", pattern: DATE_PATTERN })),
88
+ end_date: Type.Optional(Type.String({ description: "截止日期 (YYYY-MM-DD)", pattern: DATE_PATTERN })),
85
89
  });
86
90
 
87
91
  // ── 客户管理 Schema ──────────────────────────────────────────
88
92
 
89
93
  const AddContact = Type.Object({
90
94
  action: Type.Literal("add_contact"),
91
- company_id: Type.String({ description: "公司 ID" }),
92
- name: Type.String({ description: "联系人姓名" }),
93
- phone: Type.Optional(Type.String({ description: "手机号" })),
94
- email: Type.Optional(Type.String({ description: "邮箱" })),
95
- company_name: Type.Optional(Type.String({ description: "联系人所在公司" })),
95
+ company_id: Type.String({ description: "公司 ID", minLength: 1 }),
96
+ name: Type.String({ description: "联系人姓名", minLength: 1, maxLength: 100 }),
97
+ phone: Type.Optional(Type.String({ description: "手机号", maxLength: 30 })),
98
+ email: Type.Optional(Type.String({ description: "邮箱", maxLength: 200 })),
99
+ company_name: Type.Optional(Type.String({ description: "联系人所在公司", maxLength: 200 })),
96
100
  tags: Type.Optional(Type.String({ description: "标签,JSON 数组格式,如 [\"VIP\",\"供应商\"]" })),
97
- notes: Type.Optional(Type.String({ description: "备注" })),
101
+ notes: Type.Optional(Type.String({ description: "备注", maxLength: 2000 })),
98
102
  });
99
103
 
100
104
  const ListContacts = Type.Object({
101
105
  action: Type.Literal("list_contacts"),
102
- company_id: Type.String({ description: "公司 ID" }),
106
+ company_id: Type.String({ description: "公司 ID", minLength: 1 }),
103
107
  tag: Type.Optional(Type.String({ description: "按标签筛选" })),
104
108
  });
105
109
 
106
110
  const UpdateContact = Type.Object({
107
111
  action: Type.Literal("update_contact"),
108
- contact_id: Type.String({ description: "联系人 ID" }),
109
- name: Type.Optional(Type.String({ description: "新姓名" })),
110
- phone: Type.Optional(Type.String({ description: "新手机号" })),
111
- email: Type.Optional(Type.String({ description: "新邮箱" })),
112
- company_name: Type.Optional(Type.String({ description: "新公司名" })),
112
+ contact_id: Type.String({ description: "联系人 ID", minLength: 1 }),
113
+ name: Type.Optional(Type.String({ description: "新姓名", minLength: 1, maxLength: 100 })),
114
+ phone: Type.Optional(Type.String({ description: "新手机号", maxLength: 30 })),
115
+ email: Type.Optional(Type.String({ description: "新邮箱", maxLength: 200 })),
116
+ company_name: Type.Optional(Type.String({ description: "新公司名", maxLength: 200 })),
113
117
  tags: Type.Optional(Type.String({ description: "新标签" })),
114
- notes: Type.Optional(Type.String({ description: "新备注" })),
115
- last_contact_date: Type.Optional(Type.String({ description: "最近联系日期 (YYYY-MM-DD)" })),
118
+ notes: Type.Optional(Type.String({ description: "新备注", maxLength: 2000 })),
119
+ last_contact_date: Type.Optional(Type.String({ description: "最近联系日期 (YYYY-MM-DD)", pattern: DATE_PATTERN })),
116
120
  });
117
121
 
118
122
  const DeleteContact = Type.Object({
119
123
  action: Type.Literal("delete_contact"),
120
- contact_id: Type.String({ description: "联系人 ID" }),
124
+ contact_id: Type.String({ description: "联系人 ID", minLength: 1 }),
121
125
  });
122
126
 
123
127
  // ── Dashboard ────────────────────────────────────────────────
@@ -130,13 +134,13 @@ const GetDashboard = Type.Object({
130
134
 
131
135
  const SetCompanySkills = Type.Object({
132
136
  action: Type.Literal("set_company_skills"),
133
- company_id: Type.String({ description: "公司 ID" }),
137
+ company_id: Type.String({ description: "公司 ID", minLength: 1 }),
134
138
  skills: Type.Array(Type.String(), { description: "OpenClaw 内置 skill 列表,如 [\"company-registration\", \"basic-finance\"]" }),
135
139
  });
136
140
 
137
141
  const GetCompanySkills = Type.Object({
138
142
  action: Type.Literal("get_company_skills"),
139
- company_id: Type.String({ description: "公司 ID" }),
143
+ company_id: Type.String({ description: "公司 ID", minLength: 1 }),
140
144
  });
141
145
 
142
146
  // ── Union Schema ─────────────────────────────────────────────
@@ -8,7 +8,7 @@
8
8
  import { Type, type Static } from "@sinclair/typebox";
9
9
  import type { OpenClawPluginApi } from "openclaw/plugin-sdk";
10
10
  import type { OpcDatabase } from "../db/index.js";
11
- import { json } from "../utils/tool-helper.js";
11
+ import { json, toolError } from "../utils/tool-helper.js";
12
12
 
13
13
  /** 内置 AI 岗位定义 */
14
14
  const BUILTIN_ROLES: Record<string, { name: string; prompt: string; skills: string[] }> = {
@@ -147,15 +147,17 @@ export function registerStaffTool(api: OpenClawPluginApi, db: OpcDatabase): void
147
147
  "UPDATE opc_staff_config SET enabled = ?, updated_at = ? WHERE company_id = ? AND role = ?",
148
148
  p.enabled ? 1 : 0, now, p.company_id, p.role,
149
149
  );
150
- return json(db.queryOne(
150
+ const staffConfig = db.queryOne(
151
151
  "SELECT * FROM opc_staff_config WHERE company_id = ? AND role = ?",
152
152
  p.company_id, p.role,
153
- ) ?? { error: "岗位配置不存在,请先调用 configure_staff 或 init_default_staff" });
153
+ );
154
+ if (!staffConfig) return toolError("岗位配置不存在,请先调用 configure_staff 或 init_default_staff", "RECORD_NOT_FOUND");
155
+ return json(staffConfig);
154
156
  }
155
157
 
156
158
  case "init_default_staff": {
157
159
  const company = db.queryOne("SELECT * FROM opc_companies WHERE id = ?", p.company_id);
158
- if (!company) return json({ error: "公司不存在" });
160
+ if (!company) return toolError("公司不存在", "COMPANY_NOT_FOUND");
159
161
 
160
162
  const now = new Date().toISOString();
161
163
  const created: string[] = [];
@@ -197,10 +199,10 @@ export function registerStaffTool(api: OpenClawPluginApi, db: OpcDatabase): void
197
199
  }
198
200
 
199
201
  default:
200
- return json({ error: `未知操作: ${(p as { action: string }).action}` });
202
+ return toolError(`未知操作: ${(p as { action: string }).action}`, "UNKNOWN_ACTION");
201
203
  }
202
204
  } catch (err) {
203
- return json({ error: err instanceof Error ? err.message : String(err) });
205
+ return toolError(err instanceof Error ? err.message : String(err), "DB_ERROR");
204
206
  }
205
207
  },
206
208
  },
@@ -10,7 +10,33 @@ export function json(data: unknown) {
10
10
  };
11
11
  }
12
12
 
13
+ /** 标准错误码 */
14
+ export type OpcErrorCode =
15
+ | "COMPANY_NOT_FOUND"
16
+ | "CONTACT_NOT_FOUND"
17
+ | "EMPLOYEE_NOT_FOUND"
18
+ | "INVOICE_NOT_FOUND"
19
+ | "CONTRACT_NOT_FOUND"
20
+ | "RECORD_NOT_FOUND"
21
+ | "INVALID_STATUS"
22
+ | "INVALID_INPUT"
23
+ | "VALIDATION_ERROR"
24
+ | "DB_ERROR"
25
+ | "UNKNOWN_ACTION"
26
+ | "UNKNOWN_ERROR";
27
+
13
28
  /** 生成标准错误响应 */
14
- export function toolError(message: string) {
15
- return json({ ok: false, error: message });
29
+ export function toolError(message: string, code?: OpcErrorCode) {
30
+ return json({ ok: false, error: true, code: code ?? "UNKNOWN_ERROR", message });
31
+ }
32
+
33
+ /** 生成字段级验证错误 */
34
+ export function validationError(field: string, message: string) {
35
+ return json({
36
+ ok: false,
37
+ error: true,
38
+ code: "VALIDATION_ERROR" as OpcErrorCode,
39
+ message: `${field}: ${message}`,
40
+ field,
41
+ });
16
42
  }
@@ -20,7 +20,6 @@ const CUSTOM_SKILLS_DIR = path.join(os.homedir(), ".openclaw", "custom-skills");
20
20
  function sendJson(res: ServerResponse, data: unknown, status = 200): void {
21
21
  res.writeHead(status, {
22
22
  "Content-Type": "application/json; charset=utf-8",
23
- "Access-Control-Allow-Origin": "*",
24
23
  });
25
24
  res.end(JSON.stringify(data));
26
25
  }
@@ -515,9 +514,9 @@ function handleAlertDismiss(db: OpcDatabase, alertId: string): unknown {
515
514
 
516
515
  /* ── HTML builder ─────────────────────────────────────────── */
517
516
 
518
- function buildPageHtml(): string {
517
+ function buildPageHtml(authRequired = false): string {
519
518
  const toolsJson = JSON.stringify(TOOL_NAMES);
520
- return '<!DOCTYPE html>\n<html lang="zh-CN">\n<head>\n<meta charset="UTF-8">\n<meta name="viewport" content="width=device-width, initial-scale=1.0">\n<title>' + "\u661F\u73AFOPC\u4E2D\u5FC3 - \u7BA1\u7406\u540E\u53F0" + '</title>\n<link rel="preconnect" href="https://fonts.googleapis.com">\n<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>\n<link href="https://fonts.googleapis.com/css2?family=Instrument+Sans:wght@400;500;600;700&family=Noto+Sans+SC:wght@400;500;600;700&display=swap" rel="stylesheet">\n<style>\n' + getCss() + '\n</style>\n</head>\n<body>\n' + getBodyHtml() + '\n<div class="toast" id="toast"></div>\n<script>\nvar TOOLS = ' + toolsJson + ';\n' + getJs() + '\n</script>\n</body>\n</html>';
519
+ return '<!DOCTYPE html>\n<html lang="zh-CN">\n<head>\n<meta charset="UTF-8">\n<meta name="viewport" content="width=device-width, initial-scale=1.0">\n<title>' + "\u661F\u73AFOPC\u4E2D\u5FC3 - \u7BA1\u7406\u540E\u53F0" + '</title>\n<link rel="preconnect" href="https://fonts.googleapis.com">\n<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>\n<link href="https://fonts.googleapis.com/css2?family=Instrument+Sans:wght@400;500;600;700&family=Noto+Sans+SC:wght@400;500;600;700&display=swap" rel="stylesheet">\n<style>\n' + getCss() + '\n</style>\n</head>\n<body>\n' + getBodyHtml() + '\n<div class="toast" id="toast"></div>\n<script>\nvar TOOLS = ' + toolsJson + ';\nvar _authRequired = ' + (authRequired ? 'true' : 'false') + ';\n' + getJs() + '\n</script>\n</body>\n</html>';
521
520
  }
522
521
 
523
522
  function getCss(): string {
@@ -791,7 +790,45 @@ function getBodyHtml(): string {
791
790
  }
792
791
 
793
792
  function getJs(): string {
794
- return "if(!localStorage.getItem('openclaw.i18n.locale')){localStorage.setItem('openclaw.i18n.locale','zh-CN');}"
793
+ return "/* Token management */"
794
+ + "\n(function(){"
795
+ + "var p=new URLSearchParams(window.location.search);"
796
+ + "var t=p.get('token');"
797
+ + "if(t){sessionStorage.setItem('opc_token',t);p.delete('token');"
798
+ + "var newUrl=window.location.pathname;"
799
+ + "var qs=p.toString();"
800
+ + "if(qs)newUrl+='?'+qs;"
801
+ + "window.history.replaceState({},'',newUrl);}"
802
+ + "})();"
803
+ + "\nvar _opcToken=sessionStorage.getItem('opc_token')||'';"
804
+ + "\nvar _origFetch=window.fetch;"
805
+ + "\nwindow.fetch=function(url,opts){"
806
+ + "opts=opts||{};"
807
+ + "if(_opcToken){"
808
+ + "opts.headers=opts.headers||{};"
809
+ + "if(typeof opts.headers==='object'&&!(opts.headers instanceof Headers)){"
810
+ + "opts.headers['Authorization']='Bearer '+_opcToken;"
811
+ + "}}"
812
+ + "return _origFetch.call(window,url,opts).then(function(r){"
813
+ + "if(r.status===401){sessionStorage.removeItem('opc_token');_opcToken='';showLoginPage();}return r;"
814
+ + "});};"
815
+ + "\nfunction showLoginPage(){"
816
+ + "document.querySelector('.layout').innerHTML="
817
+ + "'<div style=\"display:flex;align-items:center;justify-content:center;width:100%;min-height:100vh;background:var(--bg)\">"
818
+ + "<div style=\"background:var(--card);border:1px solid var(--bd);border-radius:12px;padding:48px;max-width:380px;width:100%;text-align:center\">"
819
+ + "<h2 style=\"font-size:20px;font-weight:700;margin-bottom:8px\">\\u661F\\u73AFOPC\\u4E2D\\u5FC3</h2>"
820
+ + "<p style=\"color:var(--tx3);font-size:13px;margin-bottom:24px\">\\u8BF7\\u8F93\\u5165\\u8BBF\\u95EE\\u4EE4\\u724C</p>"
821
+ + "<input id=\"login-token\" type=\"password\" placeholder=\"Gateway Token\" style=\"width:100%;padding:10px 12px;border:1px solid var(--bd);border-radius:6px;font-size:14px;margin-bottom:16px;outline:none\"/>"
822
+ + "<button onclick=\"doLogin()\" style=\"width:100%;padding:10px;background:var(--pri);color:#fff;border:none;border-radius:6px;font-size:14px;font-weight:600;cursor:pointer\">\\u767B\\u5F55</button>"
823
+ + "</div></div>';}"
824
+ + "\nfunction doLogin(){"
825
+ + "var v=document.getElementById('login-token').value.trim();"
826
+ + "if(!v)return;"
827
+ + "sessionStorage.setItem('opc_token',v);_opcToken=v;"
828
+ + "window.location.reload();}"
829
+ + "\nif(_authRequired&&!_opcToken&&window.location.pathname.indexOf('/opc/admin')===0){"
830
+ + "document.addEventListener('DOMContentLoaded',function(){showLoginPage();});}"
831
+ + "\nif(!localStorage.getItem('openclaw.i18n.locale')){localStorage.setItem('openclaw.i18n.locale','zh-CN');}"
795
832
  + "\nvar toolConfig={};"
796
833
  + "var companiesState={search:'',status:'',page:1};"
797
834
  + "var currentView='dashboard';"
@@ -1322,7 +1359,7 @@ function getJs(): string {
1322
1359
  + "\nfunction toggleStaff(staffId,companyId,role,enabled){"
1323
1360
  + "fetch('/opc/admin/api/staff/'+encodeURIComponent(staffId)+'/toggle',{method:'PATCH',headers:{'Content-Type':'application/json'},body:JSON.stringify({enabled:enabled?1:0})})"
1324
1361
  + ".then(function(r){return r.json()}).then(function(d){if(d.ok){showToast(enabled?'\\u5df2\\u542f\\u7528 '+role:'\\u5df2\\u505c\\u7528 '+role);}"
1325
- + "else{showToast(d.error||'\\u64cd\\u4f5c\\u5931\\u8d25');}}).catch(function(){showToast('\\u8bf7\\u6c42\\u5931\\u8d25');});}"
1362
+ + "else{showToast(d.message||d.error||'\\u64cd\\u4f5c\\u5931\\u8d25');}}).catch(function(){showToast('\\u8bf7\\u6c42\\u5931\\u8d25');});}"
1326
1363
  + "\nfunction editStaff(staffId,role,roleName,companyId){"
1327
1364
  + "var existing=document.getElementById('edit-modal');if(existing)existing.remove();"
1328
1365
  + "fetch('/opc/admin/api/staff/'+encodeURIComponent(staffId)).then(function(r){return r.json()}).then(function(s){"
@@ -1345,7 +1382,7 @@ function getJs(): string {
1345
1382
  + "var data={role_name:document.getElementById('sf-name').value,system_prompt:document.getElementById('sf-prompt').value,notes:document.getElementById('sf-notes').value};"
1346
1383
  + "fetch('/opc/admin/api/staff/'+encodeURIComponent(staffId)+'/edit',{method:'PATCH',headers:{'Content-Type':'application/json'},body:JSON.stringify(data)})"
1347
1384
  + ".then(function(r){return r.json()}).then(function(d){if(d.ok){document.getElementById('edit-modal').remove();showToast('\\u5df2\\u4fdd\\u5b58');showCompany(companyId);}"
1348
- + "else{showToast(d.error||'\\u4fdd\\u5b58\\u5931\\u8d25');}}).catch(function(){showToast('\\u8bf7\\u6c42\\u5931\\u8d25');});}"
1385
+ + "else{showToast(d.message||d.error||'\\u4fdd\\u5b58\\u5931\\u8d25');}}).catch(function(){showToast('\\u8bf7\\u6c42\\u5931\\u8d25');});}"
1349
1386
  + "\nfunction addEmployee(companyId){"
1350
1387
  + "var existing=document.getElementById('edit-modal');if(existing)existing.remove();"
1351
1388
  + "var today=new Date().toISOString().slice(0,10);"
@@ -1366,7 +1403,7 @@ function getJs(): string {
1366
1403
  + "\nfunction saveEmployee(companyId){"
1367
1404
  + "var data={company_id:companyId,employee_name:document.getElementById('emp-name').value,position:document.getElementById('emp-pos').value,salary:parseFloat(document.getElementById('emp-salary').value)||0,contract_type:document.getElementById('emp-type').value,start_date:document.getElementById('emp-date').value};"
1368
1405
  + "fetch('/opc/admin/api/hr/create',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(data)})"
1369
- + ".then(function(r){return r.json()}).then(function(d){if(d.ok){document.getElementById('edit-modal').remove();showToast('\\u5458\\u5de5\\u5df2\\u6dfb\\u52a0');showCompany(companyId);}else{showToast(d.error||'\\u4fdd\\u5b58\\u5931\\u8d25');}}).catch(function(){showToast('\\u8bf7\\u6c42\\u5931\\u8d25');});}"
1406
+ + ".then(function(r){return r.json()}).then(function(d){if(d.ok){document.getElementById('edit-modal').remove();showToast('\\u5458\\u5de5\\u5df2\\u6dfb\\u52a0');showCompany(companyId);}else{showToast(d.message||d.error||'\\u4fdd\\u5b58\\u5931\\u8d25');}}).catch(function(){showToast('\\u8bf7\\u6c42\\u5931\\u8d25');});}"
1370
1407
  + "\nfunction createProject(companyId){"
1371
1408
  + "var existing=document.getElementById('edit-modal');if(existing)existing.remove();"
1372
1409
  + "var html='<div id=\"edit-modal\" style=\"position:fixed;inset:0;background:rgba(0,0,0,.45);z-index:200;display:flex;align-items:center;justify-content:center\">';"
@@ -1388,12 +1425,12 @@ function getJs(): string {
1388
1425
  + "\nfunction saveProject(companyId){"
1389
1426
  + "var data={company_id:companyId,name:document.getElementById('pj-name').value,description:document.getElementById('pj-desc').value,start_date:document.getElementById('pj-start').value,end_date:document.getElementById('pj-end').value,budget:parseFloat(document.getElementById('pj-budget').value)||0};"
1390
1427
  + "fetch('/opc/admin/api/projects/create',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(data)})"
1391
- + ".then(function(r){return r.json()}).then(function(d){if(d.ok){document.getElementById('edit-modal').remove();showToast('\\u9879\\u76ee\\u5df2\\u521b\\u5efa');showCompany(companyId);}else{showToast(d.error||'\\u4fdd\\u5b58\\u5931\\u8d25');}}).catch(function(){showToast('\\u8bf7\\u6c42\\u5931\\u8d25');});}"
1428
+ + ".then(function(r){return r.json()}).then(function(d){if(d.ok){document.getElementById('edit-modal').remove();showToast('\\u9879\\u76ee\\u5df2\\u521b\\u5efa');showCompany(companyId);}else{showToast(d.message||d.error||'\\u4fdd\\u5b58\\u5931\\u8d25');}}).catch(function(){showToast('\\u8bf7\\u6c42\\u5931\\u8d25');});}"
1392
1429
  + "\nfunction initDefaultStaff(companyId){"
1393
1430
  + "fetch('/opc/admin/api/staff/'+encodeURIComponent(companyId)+'/init',{method:'POST'})"
1394
1431
  + ".then(function(r){return r.json()}).then(function(d){"
1395
1432
  + "if(d.ok){showToast('\\u5df2\\u521d\\u59cb\\u5316 '+d.created+' \\u4e2a\\u5c97\\u4f4d');showCompany(companyId);}"
1396
- + "else{showToast(d.error||'\\u521d\\u59cb\\u5316\\u5931\\u8d25');}}).catch(function(){showToast('\\u8bf7\\u6c42\\u5931\\u8d25');});}"
1433
+ + "else{showToast(d.message||d.error||'\\u521d\\u59cb\\u5316\\u5931\\u8d25');}}).catch(function(){showToast('\\u8bf7\\u6c42\\u5931\\u8d25');});}"
1397
1434
  // ── 合同编辑 ──
1398
1435
  + "\nfunction editContract(id,title,counterparty,amount,status,startDate,endDate,keyTerms,riskNotes){"
1399
1436
  + "var existing=document.getElementById('edit-modal');if(existing)existing.remove();"
@@ -1427,7 +1464,7 @@ function getJs(): string {
1427
1464
  + "key_terms:document.getElementById('ct-terms').value,notes:document.getElementById('ct-risk').value};"
1428
1465
  + "fetch('/opc/admin/api/contracts/'+encodeURIComponent(id)+'/edit',{method:'PATCH',headers:{'Content-Type':'application/json'},body:JSON.stringify(data)})"
1429
1466
  + ".then(function(r){return r.json()}).then(function(d){if(d.ok){document.getElementById('edit-modal').remove();showToast('\\u5408\\u540c\\u5df2\\u4fdd\\u5b58');showCompany(window.currentCompanyId||'');}"
1430
- + "else{showToast(d.error||'\\u4fdd\\u5b58\\u5931\\u8d25');}}).catch(function(){showToast('\\u8bf7\\u6c42\\u5931\\u8d25');});}"
1467
+ + "else{showToast(d.message||d.error||'\\u4fdd\\u5b58\\u5931\\u8d25');}}).catch(function(){showToast('\\u8bf7\\u6c42\\u5931\\u8d25');});}"
1431
1468
  // ── createContract ──
1432
1469
  + "\nfunction createContract(companyId){"
1433
1470
  + "var existing=document.getElementById('edit-modal');if(existing)existing.remove();"
@@ -1459,7 +1496,7 @@ function getJs(): string {
1459
1496
  + "var data={company_id:companyId,title:document.getElementById('nc-title').value,counterparty:document.getElementById('nc-counterparty').value,contract_type:document.getElementById('nc-type').value,amount:parseFloat(document.getElementById('nc-amount').value)||0,start_date:document.getElementById('nc-start').value,end_date:document.getElementById('nc-end').value,key_terms:document.getElementById('nc-terms').value,risk_notes:document.getElementById('nc-risk').value};"
1460
1497
  + "fetch('/opc/admin/api/contracts/create',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(data)})"
1461
1498
  + ".then(function(r){return r.json()}).then(function(d){if(d.ok){document.getElementById('edit-modal').remove();showToast('\\u5408\\u540c\\u5df2\\u65b0\\u5efa');showCompany(companyId);}"
1462
- + "else{showToast(d.error||'\\u65b0\\u5efa\\u5931\\u8d25');}}).catch(function(){showToast('\\u8bf7\\u6c42\\u5931\\u8d25');});}"
1499
+ + "else{showToast(d.message||d.error||'\\u65b0\\u5efa\\u5931\\u8d25');}}).catch(function(){showToast('\\u8bf7\\u6c42\\u5931\\u8d25');});}"
1463
1500
  // ── addTransaction ──
1464
1501
  + "\nfunction addTransaction(companyId){"
1465
1502
  + "var existing=document.getElementById('edit-modal');if(existing)existing.remove();"
@@ -1484,7 +1521,7 @@ function getJs(): string {
1484
1521
  + "var data={company_id:companyId,type:document.getElementById('tx-type').value,category:document.getElementById('tx-category').value,amount:parseFloat(document.getElementById('tx-amount').value)||0,description:document.getElementById('tx-desc').value,counterparty:document.getElementById('tx-counterparty').value,transaction_date:document.getElementById('tx-date').value};"
1485
1522
  + "fetch('/opc/admin/api/transactions/create',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(data)})"
1486
1523
  + ".then(function(r){return r.json()}).then(function(d){if(d.ok){document.getElementById('edit-modal').remove();showToast('\\u4ea4\\u6613\\u5df2\\u65b0\\u589e');showCompany(companyId);}"
1487
- + "else{showToast(d.error||'\\u65b0\\u589e\\u5931\\u8d25');}}).catch(function(){showToast('\\u8bf7\\u6c42\\u5931\\u8d25');});}"
1524
+ + "else{showToast(d.message||d.error||'\\u65b0\\u589e\\u5931\\u8d25');}}).catch(function(){showToast('\\u8bf7\\u6c42\\u5931\\u8d25');});}"
1488
1525
  // ── editCompany (内联编辑) ──
1489
1526
  + "\nfunction editCompany(id,name,industry,ownerName,ownerContact,desc,capital,status){"
1490
1527
  + "var html='<div id=\"edit-modal\" style=\"position:fixed;inset:0;background:rgba(0,0,0,.45);z-index:200;display:flex;align-items:center;justify-content:center\">';"
@@ -1516,7 +1553,7 @@ function getJs(): string {
1516
1553
  + "fetch('/opc/admin/api/companies/'+encodeURIComponent(id)+'/edit',{method:'PATCH',headers:{'Content-Type':'application/json'},body:JSON.stringify(data)})"
1517
1554
  + ".then(function(r){return r.json()}).then(function(d){"
1518
1555
  + "if(d.ok){document.getElementById('edit-modal').remove();showToast('\\u4fdd\\u5b58\\u6210\\u529f');loadCompanyDetail(id);}"
1519
- + "else{showToast(d.error||'\\u4fdd\\u5b58\\u5931\\u8d25');}}).catch(function(){showToast('\\u64cd\\u4f5c\\u5931\\u8d25');});}"
1556
+ + "else{showToast(d.message||d.error||'\\u4fdd\\u5b58\\u5931\\u8d25');}}).catch(function(){showToast('\\u64cd\\u4f5c\\u5931\\u8d25');});}"
1520
1557
  // ── loadGuide (SOP) ──
1521
1558
  + "\nfunction loadGuide(){var el=document.getElementById('guide-content');if(!el)return;el.innerHTML=renderSopGuide();}"
1522
1559
  + getGuideJs()
@@ -2615,7 +2652,7 @@ function getCanvasJs(): string {
2615
2652
 
2616
2653
  /* ── Route registration ───────────────────────────────────── */
2617
2654
 
2618
- export function registerConfigUi(api: OpenClawPluginApi, db: OpcDatabase): void {
2655
+ export function registerConfigUi(api: OpenClawPluginApi, db: OpcDatabase, gatewayToken?: string): void {
2619
2656
  api.registerHttpHandler(async (req, res) => {
2620
2657
  const rawUrl = req.url ?? "";
2621
2658
  const urlObj = new URL(rawUrl, "http://localhost");
@@ -2626,6 +2663,23 @@ export function registerConfigUi(api: OpenClawPluginApi, db: OpcDatabase): void
2626
2663
  return false;
2627
2664
  }
2628
2665
 
2666
+ // API 端点需要认证
2667
+ if (pathname.startsWith("/opc/admin/api/") && gatewayToken) {
2668
+ if (method === "OPTIONS") {
2669
+ res.writeHead(204);
2670
+ res.end();
2671
+ return true;
2672
+ }
2673
+ const authHeader = req.headers["authorization"];
2674
+ const match = authHeader?.match(/^Bearer\s+(.+)$/i);
2675
+ const token = match?.[1];
2676
+ if (token !== gatewayToken) {
2677
+ res.writeHead(401, { "Content-Type": "application/json; charset=utf-8" });
2678
+ res.end(JSON.stringify({ error: "认证令牌无效", code: "AUTH_INVALID" }));
2679
+ return true;
2680
+ }
2681
+ }
2682
+
2629
2683
  try {
2630
2684
  // Config API: GET
2631
2685
  if (pathname === "/opc/admin/api/config" && method === "GET") {
@@ -3488,7 +3542,7 @@ export function registerConfigUi(api: OpenClawPluginApi, db: OpcDatabase): void
3488
3542
  }
3489
3543
 
3490
3544
  // Serve HTML page for all other /opc/admin paths
3491
- sendHtml(res, buildPageHtml());
3545
+ sendHtml(res, buildPageHtml(!!gatewayToken));
3492
3546
  return true;
3493
3547
  } catch (err) {
3494
3548
  res.writeHead(500, { "Content-Type": "application/json; charset=utf-8" });