from pathlib import Path p=Path('C:/kefu/wechat_rpa/knowledge_worker.py') s=p.read_text(encoding='utf-8') s=s.replace('def enhance(candidate, database):','def enhance(candidate, database, options=None):') start=s.index(' from model_gateway import Catalog, call_outlet') end=s.index(' prompt = ',start) s=s[:start]+''' from model_gateway import call_outlet from knowledge_models import resolve_model, safe_model_error import httpx options = dict(options or {}) outlet = resolve_model(database, options) deadline = options['model_timeout_seconds'] # A separate catalog instance: task timeouts do not change interactive chat settings. outlet.config = {**outlet.config, 'timeout_ms': deadline * 1000} '''+s[end:] s=s.replace('return await call_outlet(client, outlets[0], [{"role": "user", "content": prompt}], deadline=12)', '''return await call_outlet(client, outlet, [{"role": "user", "content": prompt}], deadline=deadline, max_tokens=4096, temperature=0.1)''') s=s.replace(' result = asyncio.run(run())\n if result.get("error") or not result.get("text"):\n raise ValueError("模型整理失败,可重试或使用规则整理")', ''' try: result = asyncio.run(run()) except (httpx.HTTPError, TimeoutError) as exc: raise ValueError(safe_model_error(type(exc).__name__)) from None if result.get("error") or not result.get("text"): raise ValueError(safe_model_error(result.get('error', '')))''') s=s.replace(' data = json.loads(raw)', ''' try: data = json.loads(raw) except json.JSONDecodeError: raise ValueError('模型输出不是完整 JSON,可能被截断或未遵循格式;请更换模型或检查上游输出限制') from None if not isinstance(data, dict): raise ValueError('模型输出应为 JSON 对象,请更换模型或检查模型提示词')''') start=s.index(' def step(self):') end=s.index('\n\ndef reindex',start) s=s[:start]+''' def step(self): job = self.claim() if job is None: return self.enqueue_incremental() try: options = json.loads(job['options_json']) if options['engine'] == 'model' and not options.get('model_provider_id'): from knowledge_models import resolve_model resolve_model(self.store.database, options) state = json.loads(job['state_json']) pending = state.pop('_pending', []) done = state.pop('_scan_done', False) if not pending and not done: remaining = options['max_messages'] - job['processed'] size = min(20 if options['engine'] == 'model' else 200, remaining) fetched = self.rows(job, max(1, size) + 1) if remaining > 0 else [] rows = fetched[:size] if size > 0 else [] for row in rows: state, emitted = feed(state, row) pending.extend(emitted) natural_end = remaining > 0 and len(fetched) <= size if natural_end: last = finish(state) if last: pending.append(last) state = {} done = natural_end or len(rows) >= remaining if done: options['completion_reason'] = 'source_exhausted' if natural_end else 'message_limit' state = {} cursor = {k: rows[-1][k] for k in ('conversation_id', 'sent_at', 'id')} if rows else json.loads(job['cursor_json']) # Persist the scan and redacted candidates before any paid request. # Pending candidates survive failure, pause and process restarts. with self.store.database.connect() as db: updated = db.execute("""UPDATE knowledge_job SET cursor_json=?,state_json=?, processed=processed+?,skipped=skipped+?,options_json=?,lease_until=?,updated_at=? WHERE id=? AND status='running' AND lease_owner=?""", ( json_text(cursor), json_text({**state, '_pending': pending, '_scan_done': done}), len(rows), sum(r['direction'] == 'unknown' or r['message_type'].lower() not in {'text', '文本', '文字', '1'} or r['status'] != 'normal' or not r['content'].strip() for r in rows), json_text(options), time.time() + 300, utc_now(), job['id'], self.owner)) if not updated.rowcount: return True while pending: candidate = pending[0] input_chars = output_chars = 0 with self.store.database.connect() as db: db.execute('BEGIN IMMEDIATE') current = db.execute("SELECT * FROM knowledge_job WHERE id=? AND lease_owner=? AND status='running'", (job['id'], self.owner)).fetchone() if current is None: return True if options['engine'] == 'model': if current['model_calls'] >= options['max_model_calls']: raise ValueError('模型调用预算已用完;已保存进度,请提高调用总上限或改用规则整理后继续') db.execute('UPDATE knowledge_job SET model_calls=model_calls+1 WHERE id=?', (job['id'],)) # Every call is bounded at <=181s, below the renewed 300s lease. db.execute('UPDATE knowledge_job SET lease_until=? WHERE id=?', (time.time() + 300, job['id'])) if options['engine'] == 'model': candidate, input_chars, output_chars = enhance(candidate, self.store.database, options) with self.store.database.connect() as db: db.execute('BEGIN IMMEDIATE') if not db.execute("SELECT 1 FROM knowledge_job WHERE id=? AND status='running' AND lease_owner=?", (job['id'], self.owner)).fetchone(): return True created = self.store.add_draft(db, job['tenant_id'], job['id'], candidate) pending = pending[1:] db.execute("""UPDATE knowledge_job SET state_json=?,created_items=created_items+?, duplicates=duplicates+?,input_chars=input_chars+?,output_chars=output_chars+?,updated_at=? WHERE id=?""", (json_text({**state, '_pending': pending, '_scan_done': done}), int(created), int(not created), input_chars, output_chars, utc_now(), job['id'])) with self.store.database.connect() as db: db.execute("""UPDATE knowledge_job SET status=?,state_json=?,lease_owner='',lease_until=0,error='', options_json=?,updated_at=? WHERE id=? AND status='running' AND lease_owner=?""", ('completed' if done else 'queued', json_text({} if done else state), json_text(options), utc_now(), job['id'], self.owner)) except Exception as exc: # Only local validation messages may be persisted; provider bodies stay private. error = str(exc)[:240] if isinstance(exc, ValueError) and not isinstance(exc, json.JSONDecodeError) else type(exc).__name__ with self.store.database.connect() as db: db.execute("UPDATE knowledge_job SET status='failed',error=?,lease_owner='',lease_until=0,updated_at=? " "WHERE id=? AND lease_owner=? AND status='running'", (error, utc_now(), job['id'], self.owner)) return True '''+s[end:] p.write_text(s,encoding='utf-8',newline='\n') # Public progress includes waiting candidates without exposing their content. p=Path('C:/kefu/wechat_rpa/knowledge_store.py'); s=p.read_text(encoding='utf-8') s=s.replace(' result.pop("state", None)', ' result["pending_items"] = len(result.get("state", {}).get("_pending", []))\n result.pop("state", None)') p.write_text(s,encoding='utf-8',newline='\n')