-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathorigin_errors.py
More file actions
479 lines (454 loc) · 19.7 KB
/
Copy pathorigin_errors.py
File metadata and controls
479 lines (454 loc) · 19.7 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
# -*- coding: utf-8 -*-
"""
origin_engine 的稳定错误码框架(clean-room 设计)
================================================
统一失败返回结构,让 AI 客户端能安全分支/重试:
{
"ok": false,
"error_code": "worksheet_not_found", # 稳定的机器可读错误码
"error": "人类可读信息",
"recoverable": true, # 该错误是否应安全重试
"next_actions": ["...", "..."], # 具体可执行的下一步行动
...
}
成功返回统一用 ok() 构造 {"ok": true, ...}。
设计要点(非抄袭,独立实现):
- 错误码是稳定的字符串常量,客户端按它分支,不解析自由文本;
- recoverable 语义:true = 修正输入后可安全重试;false = 环境/依赖问题,别盲目重试;
- next_actions 给出模型可以直接照着做的恢复步骤,减少瞎试;
- RECOVERY_MAP 是"错误码 -> 恢复动作"映射表(policy 重试策略 + diagnose 诊断动作),
fail() 自动把对应项内嵌到失败返回的 recovery 字段;recovery_info() 可单独查询。
"""
from __future__ import annotations
from typing import Any, Dict, Iterable, Optional
# ---------------------------------------------------------------------------
# 错误码目录:code -> (recoverable, 默认下一步行动)
# ---------------------------------------------------------------------------
CODE_META: Dict[str, tuple] = {
"connection_error": (
False,
["确认本机已安装 Origin 并已启动(或允许脚本自动启动)",
"关闭多余的 Origin64 进程,只保留一个主实例后重试",
"若 Origin 以管理员权限运行而脚本不是,改用相同权限重试"],
),
"worksheet_not_found": (
True,
["用 origin_list_project / origin_status 查看当前工作簿",
"确认传入的工作表引用形如 [Book1]Sheet1,并重新调用"],
),
"graph_not_found": (
True,
["用 origin_list_graphs 查看当前图页短名",
"确认传入的 graph 引用正确后重试"],
),
"column_not_found": (
True,
["先用 origin_read_worksheet 查看列名列表",
"传入正确的列名(长名优先)或 0 起始列索引"],
),
"invalid_request": (
True,
["核对参数取值(枚举/范围/格式)",
"按返回的合法取值列表重新调用"],
),
"empty_data": (
True,
["先写入或导入数据(origin_write_data / origin_import_file)再调用"],
),
"no_such_column_ref": (
True,
["核对列引用是否正确(长名/短名/索引)", "参考 origin_read_worksheet 返回的列清单"],
),
"unsupported_origin_feature": (
False,
["该能力需要 OriginPro 或更高版本 Origin", "改用降级方案(如参数化入口的其他 kind)"],
),
"origin_operation_error": (
False,
["查看 error 字段的具体异常信息",
"若是有害的数据/参数异常,修正后重试;若是 Origin 内部错误,重启 Origin 后再试"],
),
"export_error": (
True,
["确认输出目录存在且有写权限", "换一个输出路径或格式后重试"],
),
"file_error": (
True,
["确认文件路径存在且可读", "修正路径后重试"],
),
"invalid_column_designation": (
True,
["核对列角色参数(X/Y/Z/忽略)取值", "参考 origin_read_worksheet 的列信息"],
),
"origin_busy_user_session": (
True,
["关闭当前 Origin 窗口后重试(isolated 模式不劫持已打开的 Origin)",
"或在启动器环境改回 ORIGIN_SESSION=attach(默认)以复用已开实例"],
),
"file_unsupported_format": (
True,
["改用 CSV/TXT/XLSX 格式", "旧版 .xls 请先在 Excel/Origin 中另存为 XLSX"],
),
"file_read_error": (
True,
["确认文件存在且未损坏、未被其他程序锁定",
"XLSX 需要 openpyxl:在插件 venv 中 pip install openpyxl 后重试"],
),
"plan_not_found": (
True,
["plan_id 已失效(服务器重启或缓存淘汰),重新调用 origin_plot_plan 生成",
"确认 plan_id 来自本次会话的 origin_plot_plan 返回值"],
),
"plan_stale": (
True,
["数据或列映射在计划生成后发生了变化:重新 origin_plot_plan 生成新计划",
"确认新计划的 plan_hash 后再 origin_plot_execute"],
),
"template_unavailable": (
True,
["查看 detail 中缺失的模板/图层能力说明",
"改用基础 origin_plot 或其他 template_id(见 origin_status 的 features.templates)"],
),
"delivery_error": (
True,
["确认目标目录可写(OneDrive/网盘同步目录可能锁定文件)",
"换一个 output_dir 后重试"],
),
"window_activation_failed": (
True,
["LabTalk 只在活动窗口内解析:先 origin_list_pages 复核图页短名",
"用 origin_manage_pages(action='activate', pages=[<短名>]) 显式激活后重试",
"若图页已隐藏/关闭,用 action='show' 或重新出图"],
),
"layer_not_found": (
True,
["用 origin_inspect_graph 查看图层数与索引(0 起始)",
"确认 layer 参数后重试"],
),
# --- v2.3.0 实测新增(column_formula / fit / 数据链路健壮性) ---
"formula_no_effect": (
True,
["复核源列确有数据、目标列引用正确后重试",
"检查公式函数名:LabTalk 以 10 为底对数是 log() 而不是 log10()(log10 会静默无效)",
"引擎已自动做 range 变量绑定与函数名纠正,仍失败说明公式本身无法解析"],
),
"no_data_to_fit": (
True,
["用 origin_read_worksheet 查看拟合列:x/y 全空无法拟合",
"先写入或修复数据(origin_write_data / origin_import_file)再拟合"],
),
"insufficient_data": (
True,
["线性拟合至少需要 3 个有效点", "补足数据点或改用 origin_stats 做描述统计"],
),
"column_empty": (
True,
["目标列长度为 0:先写入数据或用 origin_column_formula 计算派生列"],
),
"column_all_nan": (
True,
["列有长度但全为空值:常见原因是上游 column_formula 未生效或源列含文本",
"用 origin_read_worksheet 复核上游数据,修复后重试"],
),
# --- v2.3.0 项目保存策略(不自动写 .opju 的受控提示) ---
"manual_save_required": (
False,
["请在 Origin 窗口按 Ctrl+S 手动保存当前项目",
"如需恢复脚本自动保存,取消环境变量 DSH_ORIGIN_NO_AUTO_SAVE 后重试"],
),
# --- v2.4.0 看门狗与 LabTalk 门禁 ---
"com_blocked_by_dialog": (
True,
["Origin 被模态对话框阻塞且看门狗未能解除(错误信息含对话框标题)",
"在 Origin 窗口手动关闭对话框后原样重试;若 Origin 已无响应,"
"taskkill /F /IM Origin64.exe 后重连(isolated 会话下次连接自动清理)"],
),
"labtalk_blocked": (
False,
["脚本包含破坏性命令(delete / doc -s / exit 等),默认拦截",
"确认无误后带 confirm=true 重发;或改用等价的非破坏命令"],
),
"path_not_allowed": (
False,
["路径不在 DSH_ORIGIN_ALLOWED_ROOTS 白名单内(安全策略拦截)",
"让用户调整白名单环境变量(分号分隔多个前缀),或把文件移入允许目录"],
),
"template_exists": (
True,
["同名模板已存在", "传 overwrite=true 覆盖,或换个模板名"],
),
"template_not_found": (
True,
["模板不存在", "origin_template_list 查看可用模板名后重试"],
),
"no_style_snapshot": (
True,
["该模板没有可套用的样式快照(可能是空模板或非本插件存的模板)",
"用 origin_template_save 从一张成品图重新存样式快照"],
),
"capability_unavailable": (
True,
["无法从本机 Origin 安装提取能力表(未找到 oPlotIDs.h)",
"确认 Origin 安装完整;或用 origin_status 查内建能力清单"],
),
"labtalk_silent_trap": (
True,
["脚本含已知静默失败命令(不报错但悄悄不干活)",
"按返回的 alternative 换替代写法;确知风险可带 force_silent=true 放行"],
),
}
VALID_CODES = frozenset(CODE_META)
# ---------------------------------------------------------------------------
# 错误码 -> 恢复动作映射表(#4)
# ---------------------------------------------------------------------------
# 每个错误码对应一段结构化恢复策略,fail() 会把精简版内嵌到失败返回里:
# policy 重试策略:
# fix_args_then_retry 修正参数/输入后重试
# retry_same_args 原样重试(瞬时故障,引擎已有自愈)
# restart_origin_then_retry 先清理 Origin 进程再重试
# replan_then_retry 数据/映射已变,重新生成计划后重试
# no_retry 不要重试(需人工介入或改策略开关)
# diagnose 恢复前建议执行的诊断动作(工具调用或系统命令)
RECOVERY_POLICY_FIX = "fix_args_then_retry"
RECOVERY_POLICY_RETRY = "retry_same_args"
RECOVERY_POLICY_RESTART = "restart_origin_then_retry"
RECOVERY_POLICY_REPLAN = "replan_then_retry"
RECOVERY_POLICY_NO = "no_retry"
RECOVERY_MAP: Dict[str, Dict[str, Any]] = {
"connection_error": {
"policy": RECOVERY_POLICY_RESTART,
"diagnose": ["origin_diagnose",
"taskkill /F /IM Origin64.exe 后等 2 秒再重连"
"(或设置 DSH_ORIGIN_AUTOKILL=1 让引擎连接前自动清理)"],
},
"worksheet_not_found": {
"policy": RECOVERY_POLICY_FIX,
"diagnose": ["origin_status", "origin_list_project"],
},
"graph_not_found": {
"policy": RECOVERY_POLICY_FIX,
"diagnose": ["origin_list_graphs"],
},
"column_not_found": {
"policy": RECOVERY_POLICY_FIX,
"diagnose": ["origin_read_worksheet"],
},
"invalid_request": {
"policy": RECOVERY_POLICY_FIX,
"diagnose": ["origin_help"],
},
"empty_data": {
"policy": RECOVERY_POLICY_FIX,
"diagnose": ["origin_write_data 或 origin_import_file"],
},
"no_such_column_ref": {
"policy": RECOVERY_POLICY_FIX,
"diagnose": ["origin_read_worksheet"],
},
"unsupported_origin_feature": {
"policy": RECOVERY_POLICY_NO,
"diagnose": ["origin_status 查看 features 能力清单"],
},
"origin_operation_error": {
"policy": RECOVERY_POLICY_RESTART,
"diagnose": ["origin_status", "origin_diagnose"],
},
"export_error": {
"policy": RECOVERY_POLICY_FIX,
"diagnose": ["origin_diagnose(含导出目录权限检查)",
"查看返回里的 attempts 字段确认三级导出通道各自失败原因"],
},
"file_error": {
"policy": RECOVERY_POLICY_FIX,
"diagnose": ["确认文件路径存在且未被锁定"],
},
"invalid_column_designation": {
"policy": RECOVERY_POLICY_FIX,
"diagnose": ["origin_read_worksheet"],
},
"origin_busy_user_session": {
"policy": RECOVERY_POLICY_RESTART,
"diagnose": ["tasklist /FI \"IMAGENAME eq Origin64.exe\"",
"taskkill /F /IM Origin64.exe 后等 2 秒再重连"
"(或设置 DSH_ORIGIN_AUTOKILL=1 自动清理)"],
},
"file_unsupported_format": {
"policy": RECOVERY_POLICY_FIX,
"diagnose": ["改用 CSV/TXT/XLSX"],
},
"file_read_error": {
"policy": RECOVERY_POLICY_FIX,
"diagnose": ["确认文件未损坏/未被占用", "安装 openpyxl(XLSX 需要)"],
},
"plan_not_found": {
"policy": RECOVERY_POLICY_REPLAN,
"diagnose": ["origin_plot_plan 重新生成计划"],
},
"plan_stale": {
"policy": RECOVERY_POLICY_REPLAN,
"diagnose": ["数据或列映射已变化:重新 origin_plot_plan 并确认后 execute"],
},
"template_unavailable": {
"policy": RECOVERY_POLICY_FIX,
"diagnose": ["origin_status 查看 features.templates"],
},
"delivery_error": {
"policy": RECOVERY_POLICY_FIX,
"diagnose": ["确认交付目录可写(网盘同步目录可能锁文件)"],
},
"window_activation_failed": {
"policy": RECOVERY_POLICY_FIX,
"diagnose": ["origin_list_pages", "origin_manage_pages(activate)"],
},
"layer_not_found": {
"policy": RECOVERY_POLICY_FIX,
"diagnose": ["origin_inspect_graph"],
},
"formula_no_effect": {
"policy": RECOVERY_POLICY_FIX,
"diagnose": ["origin_read_worksheet 复核源列/目标列",
"检查函数名:LabTalk 底 10 对数是 log() 不是 log10()"],
},
"no_data_to_fit": {
"policy": RECOVERY_POLICY_FIX,
"diagnose": ["origin_read_worksheet 查看 x/y 列数据"],
},
"insufficient_data": {
"policy": RECOVERY_POLICY_FIX,
"diagnose": ["确认有效点数(线性拟合需 ≥3)"],
},
"column_empty": {
"policy": RECOVERY_POLICY_FIX,
"diagnose": ["origin_read_worksheet 复核列长度"],
},
"column_all_nan": {
"policy": RECOVERY_POLICY_FIX,
"diagnose": ["origin_read_worksheet 复核上游 column_formula / 数据写入"],
},
"manual_save_required": {
"policy": RECOVERY_POLICY_NO,
"diagnose": ["在 Origin 窗口按 Ctrl+S 手动保存",
"或设置 DSH_ORIGIN_NO_AUTO_SAVE=0 恢复自动保存"],
},
"com_blocked_by_dialog": {
"policy": RECOVERY_POLICY_RETRY,
"diagnose": ["在 Origin 窗口关闭阻塞对话框(标题见 error 字段)",
"origin_status 复核连接;必要时 taskkill /F /IM Origin64.exe"],
},
"labtalk_blocked": {
"policy": RECOVERY_POLICY_FIX,
"diagnose": ["检查脚本是否确需破坏性命令",
"确认后带 confirm=true 重发 origin_labtalk"],
},
"path_not_allowed": {
"policy": RECOVERY_POLICY_NO,
"diagnose": ["检查 DSH_ORIGIN_ALLOWED_ROOTS 白名单设置",
"把文件移入允许目录或请用户扩白名单"],
},
"template_exists": {
"policy": RECOVERY_POLICY_FIX,
"diagnose": ["换模板名,或确认覆盖后传 overwrite=true"],
},
"template_not_found": {
"policy": RECOVERY_POLICY_FIX,
"diagnose": ["origin_template_list 查可用模板名",
"origin_template_save 先从成品图存一个"],
},
"no_style_snapshot": {
"policy": RECOVERY_POLICY_FIX,
"diagnose": ["该模板无可套用样式;用 origin_template_save 重新存快照"],
},
"capability_unavailable": {
"policy": RECOVERY_POLICY_REPLAN,
"diagnose": ["origin_diagnose 确认 Origin 安装与路径",
"origin_status 查内建能力清单作为替代"],
},
"labtalk_silent_trap": {
"policy": RECOVERY_POLICY_FIX,
"diagnose": ["读 alternative 字段里的替代写法并改写脚本",
"COMPATIBILITY.md 有完整静默失败清单"],
},
}
_DEFAULT_RECOVERY = {
"policy": RECOVERY_POLICY_RESTART,
"diagnose": ["origin_status", "origin_diagnose"],
}
def recovery_info(code: str) -> Dict[str, Any]:
"""返回错误码的恢复动作映射项;未知码退回通用恢复策略。"""
return RECOVERY_MAP.get(code, _DEFAULT_RECOVERY)
def code_info(code: str) -> tuple:
"""返回 (recoverable, next_actions);未知码退回通用处理。"""
info = CODE_META.get(code)
if info is None:
return CODE_META["origin_operation_error"]
return info
def ok(**fields: Any) -> dict:
"""构造成功返回:ok 置顶。"""
return {"ok": True, **fields}
def fail(code: str, error: Optional[str] = None, *,
next_actions: Optional[Iterable[str]] = None,
trace: Optional[str] = None, **extra: Any) -> dict:
"""构造失败返回。error 缺省时给出该码的一段默认说明。
自动内嵌该错误码的恢复动作(recovery.policy / recovery.diagnose),
调用方可用 **extra 里的同名键覆盖。
"""
recoverable, default_actions = code_info(code)
payload = {
"ok": False,
"error_code": code,
"error": error or f"{code}",
"recoverable": recoverable,
"next_actions": list(next_actions) if next_actions is not None else list(default_actions),
"recovery": recovery_info(code),
}
if trace:
payload["trace"] = trace
payload.update(extra)
return payload
# ---------------------------------------------------------------------------
# 例外 -> 错误码 的启发式归类(不解析具体厂商文案,只按通用特征)
# ---------------------------------------------------------------------------
def _classify_exception(exc: Exception) -> str:
msg = str(exc).lower()
text = f"{type(exc).__name__} {msg}"
if any(k in text for k in ("not connected", "connection", "no connection",
"server", "attach", "automation error")):
return "connection_error"
if any(k in text for k in ("worksheet", "sheet", "book1", "wks") ) and \
any(k in text for k in ("not found", "no such", "does not exist", "failed to find")):
return "worksheet_not_found"
if "graph" in text and any(k in text for k in ("not found", "no such", "does not exist")):
return "graph_not_found"
if any(k in text for k in ("column", "col(1", "colindex")) and \
any(k in text for k in ("not found", "invalid", "bad index", "out of range")):
return "column_not_found"
return "origin_operation_error"
def from_exception(exc: Exception, *, prefix: Optional[str] = None,
trace: Optional[str] = None) -> dict:
"""把任意异常转成统一失败返回。"""
code = _classify_exception(exc)
msg = str(exc) or type(exc).__name__
if prefix:
msg = f"{prefix}{msg}"
return fail(code, error=msg, trace=trace)
# ---------------------------------------------------------------------------
# 小工具:把引擎的旧式 {"ok": false, "error": ...} 快速升级为统一结构
# ---------------------------------------------------------------------------
def upgrade_legacy_failure(result: dict) -> dict:
"""若传入的是旧式失败 dict,按 error 文本猜测错误码升级;已是新式则原样返回。"""
if result.get("ok"):
return result
if "error_code" in result:
return result
code = "origin_operation_error"
text = str(result.get("error", "")).lower()
if any(k in text for k in ("列不存在", "column", "no such col")):
code = "column_not_found"
elif any(k in text for k in ("工作表不存在", "no such sheet", "workbook", "book not")):
code = "worksheet_not_found"
elif any(k in text for k in ("not found", "不存在", "无输出", "创建失败")):
code = "graph_not_found" if "图" in text else "origin_operation_error"
elif any(k in text for k in ("必须是", "参数", "取值", "收到", "invalid")):
code = "invalid_request"
return fail(code, error=str(result.get("error", "")),
**{k: v for k, v in result.items() if k not in ("ok", "error")})