sync_src_v3.py 9.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272
  1. PATH = '/data/ai/audio/server/src/services/llm/index.ts'
  2. with open(PATH, 'r', encoding='utf-8') as f:
  3. src = f.read()
  4. # 1. 在 import 区域加 AsyncLocalStorage
  5. if 'AsyncLocalStorage' not in src:
  6. # 找一个 import 区块
  7. src = src.replace(
  8. "import { AsyncLocalStorage } from 'async_hooks';",
  9. "import { AsyncLocalStorage } from 'async_hooks';",
  10. 0
  11. )
  12. # 实际上还没 import,需要在文件头部加
  13. # 找到第一个 import 行
  14. import_line = "import { AsyncLocalStorage } from 'async_hooks';\n"
  15. if 'import { AsyncLocalStorage' not in src:
  16. # 找第一个 import 行的位置之前插入
  17. first_import = src.find('import ')
  18. src = src[:first_import] + import_line + src[first_import:]
  19. # 2. 在 findVendorForModel 之后加 ALS helper + resolveNodeForModel + pickNextModelAfterFailure
  20. # 找位置:findVendorForModel 函数体结束
  21. helper_code = """
  22. // ============ HA: AsyncLocalStorage 传递 tried nodes ============
  23. const _triedNodesStorage = new AsyncLocalStorage<Set<string>>();
  24. function _runWithTriedNodes<T>(triedSet: Set<string>, fn: () => T): T {
  25. return _triedNodesStorage.run(triedSet, fn);
  26. }
  27. function _currentTriedNodes(): Set<string> {
  28. return _triedNodesStorage.getStore() || new Set();
  29. }
  30. /** 根据 modelId + tried set 找到下一个未尝试的 provider node */
  31. function resolveNodeForModel(modelId: string, tried: Map<string, any>): any | null {
  32. const registry = getLlmRegistry();
  33. for (const n of registry.listAvailable()) {
  34. if (!n.provider.hasModel(modelId)) continue;
  35. const fp = n.provider.vendor + ':' + (n.provider._apiKey || '').slice(-12);
  36. if (!tried.has(fp)) return n;
  37. }
  38. return null;
  39. }
  40. /** 从当前 node 找下一个未 tried 的 node, 返回 next modelId */
  41. function pickNextModelAfterFailure(currentModelId: string, tried: Map<string, any>, currentNode: any): string | null {
  42. const registry = getLlmRegistry();
  43. const available = registry.listAvailable();
  44. if (available.length === 0) return null;
  45. const startIndex = currentNode ? (available.indexOf(currentNode) + 1) % available.length : 0;
  46. for (let i = 0; i < available.length; i++) {
  47. const idx = (startIndex + i) % available.length;
  48. const node = available[idx];
  49. if (node === currentNode) continue;
  50. const fp = node.provider.vendor + ':' + (n.provider._apiKey || '').slice(-12);
  51. if (tried.has(fp)) continue;
  52. if (node.provider.hasModel(currentModelId)) {
  53. console.log(`[LLM] 同模型切换到供应商 ${node.provider.displayName},模型 ${currentModelId}`);
  54. return currentModelId;
  55. }
  56. for (const modelId of node.provider.textModels) {
  57. const cfg = node.provider.getModelConfig(modelId);
  58. if (cfg?.canonicalModel === currentModelId) {
  59. console.log(`[LLM] 别名模型切换到供应商 ${node.provider.displayName},模型 ${modelId}`);
  60. return modelId;
  61. }
  62. }
  63. if (node.provider.textModels.length > 0) {
  64. const nextId = node.provider.textModels[0];
  65. console.log(`[LLM] 切换到供应商 ${node.provider.displayName},模型 ${nextId}`);
  66. return nextId;
  67. }
  68. }
  69. return null;
  70. }
  71. """
  72. # fix typo: (n.provider._apiKey should be (node.provider._apiKey
  73. helper_code = helper_code.replace("(n.provider._apiKey || '')", "(node.provider._apiKey || '')")
  74. # 插在 findVendorForModel 之后
  75. if 'resolveNodeForModel' not in src:
  76. # 找 findVendorForModel 结束的位置
  77. fvf_marker = "function findVendorForModel"
  78. idx = src.find(fvf_marker)
  79. if idx < 0:
  80. print("ERROR: findVendorForModel not found")
  81. raise SystemExit(1)
  82. # 找函数体结束的 '}'(找匹配的最外层)
  83. depth = 0
  84. i = src.find('{', idx)
  85. end = -1
  86. while i < len(src):
  87. if src[i] == '{': depth += 1
  88. elif src[i] == '}':
  89. depth -= 1
  90. if depth == 0:
  91. end = i + 1
  92. break
  93. i += 1
  94. if end < 0:
  95. print("ERROR: findVendorForModel body not found")
  96. raise SystemExit(1)
  97. src = src[:end] + "\n" + helper_code + src[end:]
  98. print("OK: helpers injected after findVendorForModel")
  99. # 3. 改 findVendorForModel 跳过 tried
  100. OLD_FVF = """function findVendorForModel(modelId) {
  101. const registry = getLlmRegistry();
  102. return registry.listAvailable().find(n => n.provider.hasModel(modelId));
  103. }"""
  104. NEW_FVF = """function findVendorForModel(modelId) {
  105. const registry = getLlmRegistry();
  106. const tried = _currentTriedNodes();
  107. for (const n of registry.listAvailable()) {
  108. if (!n.provider.hasModel(modelId)) continue;
  109. const fp = n.provider.vendor + ':' + (n.provider._apiKey || '').slice(-12);
  110. if (tried.has(fp)) continue;
  111. return n;
  112. }
  113. return registry.listAvailable().find(n => n.provider.hasModel(modelId)) || null;
  114. }"""
  115. if OLD_FVF in src:
  116. src = src.replace(OLD_FVF, NEW_FVF)
  117. print("OK: findVendorForModel patched")
  118. else:
  119. print("WARN: findVendorForModel old pattern not found")
  120. # 4. 改 invokeWithRetry (v3 整段)
  121. # 暂不改 src invokeWithRetry — dist 的 v3 在跑, src 等下次有精力再合
  122. # 但要保证 tsc 不报错
  123. # 实际 tsc 不会编译 dist 用的代码到 dist, 所以 dist 的 v3 是 node 跑时直接要的
  124. # 跑 npm run build 才会重编 src → 覆盖 dist
  125. # 所以我们必须改 src invokeWithRetry
  126. # 找 invokeWithRetry 在 src 的位置
  127. inv_marker = "async function invokeWithRetry"
  128. idx = src.find(inv_marker)
  129. print(f"invokeWithRetry starts at line {src[:idx].count(chr(10))+1}")
  130. # 看 src 当前 invokeWithRetry 大致行数
  131. # 直接 find 函数结束
  132. depth = 0
  133. i = src.find('{', idx)
  134. end = -1
  135. while i < len(src):
  136. if src[i] == '{': depth += 1
  137. elif src[i] == '}':
  138. depth -= 1
  139. if depth == 0:
  140. end = i + 1
  141. break
  142. i += 1
  143. if end < 0:
  144. print("ERROR: invokeWithRetry body not found")
  145. raise SystemExit(1)
  146. # print(src[idx:end][:500])
  147. # print("---")
  148. # print(src[idx:end][-200:])
  149. # 替换为 v3 版本
  150. V3 = """async function invokeWithRetry(
  151. fn: (modelId: string) => Promise<any>,
  152. modelId: string,
  153. callType: string = 'llm_chat',
  154. ): Promise<any> {
  155. const tried = new Map<string, any>();
  156. let cur = modelId;
  157. let lastErr: any = null;
  158. for (let round = 0; round < HA_RETRY_CONFIG.maxFallbackRounds; round++) {
  159. // 关键: 选下一个未 tried 的 node (而不是用 resolveProviderKey 取第一个匹配)
  160. const node = resolveNodeForModel(cur, tried);
  161. if (!node) {
  162. console.warn(`[LLM-HA] 已尝试所有 node,停止 fallback`);
  163. break;
  164. }
  165. const fp = node.provider.vendor + ':' + (node.provider._apiKey || '').slice(-12);
  166. tried.set(fp, { model: cur });
  167. let providerError: any = null;
  168. for (let attempt = 0; attempt < HA_RETRY_CONFIG.maxRetriesPerProvider; attempt++) {
  169. try {
  170. const result = await _runWithTriedNodes(new Set(tried.keys()), () => fn(cur));
  171. return result;
  172. } catch (error: any) {
  173. const em = error?.message || String(error);
  174. lastErr = error;
  175. providerError = error;
  176. if (isBusinessError(em) && !isNetworkError(em)) {
  177. console.error(`[LLM-HA] ${fp}/${cur} 业务错误,立即抛出: ${em.substring(0, 100)}`);
  178. throw error;
  179. }
  180. if (isNetworkError(em)) {
  181. console.warn(`[LLM-HA] ${fp}/${cur} 网络错误,立即切换: ${em.substring(0, 100)}`);
  182. break;
  183. }
  184. if (attempt < HA_RETRY_CONFIG.maxRetriesPerProvider - 1) {
  185. const delay = backoffWithJitter(attempt);
  186. console.log(`[LLM-HA] ${fp}/${cur} 第${attempt + 1}次失败: ${em.substring(0, 100)}, ${delay}ms后重试`);
  187. await new Promise(r => setTimeout(r, delay));
  188. }
  189. }
  190. }
  191. const em = providerError?.message || 'unknown';
  192. const et = isNetworkError(em) ? 'network' : (isBusinessError(em) ? 'business' : 'unknown');
  193. tried.set(fp, { model: cur, error: em, attempts: HA_RETRY_CONFIG.maxRetriesPerProvider, errorType: et });
  194. console.log(`[LLM-HA] ${fp}/${cur} 失败 (${et}),尝试切换供应商`);
  195. const next = pickNextModelAfterFailure(cur, tried, node);
  196. if (!next) {
  197. console.warn(`[LLM-HA] 找不到下一个未尝试的供应商`);
  198. break;
  199. }
  200. cur = next;
  201. console.log(`[LLM-HA] 切换到 ${cur}`);
  202. }
  203. throw new AllProvidersFailedError(
  204. Array.from(tried.entries()).map(([provider, info]) => ({ provider, ...info }))
  205. );
  206. }"""
  207. # 找到调用 trySwitchModel 的位置,确认范围
  208. # 用更可靠的方法:找 "async function invokeWithRetry" 到 "throw new AllProvidersFailedError" 的最外层
  209. import re
  210. m = re.search(r'async function invokeWithRetry[^{]*\{', src)
  211. if not m:
  212. print("ERROR: invokeWithRetry def not found")
  213. raise SystemExit(1)
  214. start = m.start()
  215. # 用括号配对找结束
  216. depth = 0
  217. i = src.find('{', start)
  218. end = -1
  219. while i < len(src):
  220. if src[i] == '{': depth += 1
  221. elif src[i] == '}':
  222. depth -= 1
  223. if depth == 0:
  224. end = i + 1
  225. break
  226. i += 1
  227. if end < 0:
  228. print("ERROR: invokeWithRetry body end not found")
  229. raise SystemExit(1)
  230. # 也包含 HA_RETRY_CONFIG / AllProvidersFailedError 等需要在文件前部定义
  231. # 检查它们是否已存在
  232. if 'HA_RETRY_CONFIG' not in src:
  233. print("ERROR: HA_RETRY_CONFIG not defined in src - run previous patch first")
  234. raise SystemExit(1)
  235. if 'class AllProvidersFailedError' not in src:
  236. print("ERROR: AllProvidersFailedError not defined in src - run previous patch first")
  237. raise SystemExit(1)
  238. old_func = src[start:end]
  239. new_func = V3
  240. src = src[:start] + new_func + src[end:]
  241. print(f"OK: invokeWithRetry replaced ({len(old_func)} -> {len(new_func)} chars)")
  242. # 5. 把 HA_RETRY_CONFIG 改为 export const (dist 也是 exports.HA_RETRY_CONFIG, src 可能要 export)
  243. # 实际 tsc 编译 src -> dist 时, exports.X 变成 module.exports.X, 正常
  244. # 但 HA_RETRY_CONFIG 在 src 是 const 吗? 让我检查
  245. # 既然前面 tsc 没报错, 应该是 const 形式
  246. with open(PATH, 'w', encoding='utf-8') as f:
  247. f.write(src)
  248. print("OK: src/services/llm/index.ts fully synced with v3")