diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index 9fa059ce..44c6e083 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -10,7 +10,7 @@ | 主体 | `subjects`、`subject_aliases`、`platform_identities`、`subject_links`、`subject_alias_blocks` | | 记忆 | `memories`、`memory_subjects`、`memory_tags`、`sources`、`memory_revisions` | | 当前状态 | `current_state`、`state_reports` | -| 自我与目标 | `persona_versions`、`persona_attempts`、`goals`、`goal_sources`、`goal_people`、`goal_memories`、`goal_duplicates`、`goal_dedup_jobs`、`goal_reminder_plans`、`notifications`、`runtime_settings` | +| 自我与目标 | `persona_versions`、`persona_attempts`、`goals`、`goal_sources`、`goal_people`、`goal_memories`、`goal_duplicates`、`goal_dedup_jobs`、`goal_auto_reviews`、`goal_duplicate_judgments`、`goal_reminder_plans`、`notifications`、`runtime_settings` | | 模型 | `model_calls` | | 召回 | `memory_fts_jieba`、`memory_fts_trigram`、`vector_dirty`、`recalls`、`recall_items` | @@ -213,6 +213,8 @@ auto_v1 的初判使用 `consolidation_pair_auto_v1.md`(report_only 仍用 pai 目标阶段每次读取最多 100 个未合并的开放目标及修订;逐目标在自己的短事务调用 `review_goal_basis(conn, current, goal_id=..., expected_revision=...)`,将依据观察、目标历史、报告项和维护游标原子提交。采用目标线的单项事务接口,是为了使报告/游标与目标变化一同回滚;独立的 review_goal_basis_items 仍供其他分页调用者使用。冲突跳过并汇总,下一次从 0 扫描;新目标不越过本次 goal_through 上界。依据恢复/再变化由目标模块结束旧标注,不自动删除或关闭目标,过期标识沿用实时投影。 +目标阶段的确定性项目处理完后,按运行时固定的两个开关(`settings_json.goal_auto`,且当前设置仍开启)依次执行 G2 和 G1(全自动阶段 G 线,默认关闭)。两者都是“只读快照取材料 → 事务外调用模型 → 单个短写事务核对修订号并写结果和报告项”:G1 为 `auto_review_material`/`judge_goal_review`/`apply_goal_review`,G2 为 `duplicate_auto_material`/`judge_duplicate_pair`/`apply_duplicate_judgment`。G2 与创建时复核共用 `dedup_review_material`、`dedup_review_decision`、`dedup_review_stale`,因此现有去重探测测到的就是维护路径的判断。指纹表 `goal_auto_reviews`、`goal_duplicate_judgments`(迁移 039)记录已完成的判断,续跑和下一次维护跳过未变化的目标;失败不记指纹。运行统计连同游标写在 `progress_json.goal_auto`,与每项结果同事务提交,续跑从游标继续并累加计数;逐项结果写 `maintenance_items`(phase=goals)。`goals.auto_review_after_message_id` 是管理员重开时的消息水位,之后的候选只取更新的消息,`_merge` 取双方较大值。G1 按最久未复核优先(忽略本次运行自己的判断,续跑顺序不变),跳过本次运行中 G2 刚合并进来的存续目标;“我”的候选消息只取目标所在入口和目标创建之后涉及的人发过言的入口,查询走迁移 039 的 `messages(sender_subject_id,entry_id,id)` 索引。G1 关闭时逐条通过核对的合格依据写入 `goal_auto_review_evidence`,`memory_ops.message_references` 据此保护它们不被消息清理删除,重开时删除这次关闭的保护。G2 按较晚一方的 `host_scope_json` 组材料和合并;较晚一方本身或已被合并进它的任一宿主目标要并进其范围外的目标时只记 kept、保留标记;自动合并在 `goal_duplicate_judgments.merge_record_json` 记下撤销所需的 ID 和字段值(不含正文),`undo_auto_merge` 在核对存续目标修订号后原样退回。 + `PATCH /admin/api/settings/consolidation` 暴露 max_calls、resolution(默认 auto_v1,可退回 report_only_v1)及 merge_enabled、conflict_enabled、dependency_enabled、persona_enabled、goal_review_enabled(全开),时间复用 lifecycle.maintenance_time;更新与审计同事务。模型项目开关进入候选指纹和待办筛选,关闭期间不消耗该类调用,重新打开可发现延期项目;合并和矛盾共用一轮分类,关闭矛盾建议时仍防止矛盾记忆合并。报告通过原维护接口汇总变化、失败、模型建议、persona 结果、目标复核和全部模型用量;无变化/未到期只汇总,记忆建议始终仅管理员可见。 ## M3 persona diff --git a/README.md b/README.md index b6dfde3a..cddffe6e 100644 --- a/README.md +++ b/README.md @@ -653,6 +653,20 @@ GD2:`pending`/运行中的目标在补判结束前不进入 prepare 和 sear 探测脚本支持仓库外 `--corpus` 和 `--out`;`--fake` 使用本地传输,不读取模型配置、不发送网络请求,结果明确标为模拟并被选型函数拒绝。使用方法与本轮验证汇总见 [探测说明](evals/goal_dedup_probe/README.md)。 +## 全自动阶段 G:目标自动复核 + +两项都在每日维护的目标阶段、确定性项目之后执行。开关 `goals.goal_auto_review_enabled` 与 `goals.goal_duplicate_auto_judge_enabled` 默认关闭,新库和旧库都不开;设置页“目标与提醒”提供两个复选框,只有显式保存过才写入运行设置。开关在接受维护请求时固定到该次运行,运行中被关闭时停止。模型调用都在事务外,写回前核对目标修订号,不一致跳过,下次再处理;不新增模型用途。两项都受用途暂停和每日 token 上限约束:G1 调用前按整理的做法检查 `chat`/learning 准入,G2 的 `goal_dedup_judge` 用途本身受上限约束;不可用时本轮停止,不记指纹,下次再试。 + +**G1 内部目标按依据完成或放弃**:只处理 `origin=internal`、`kind=normal`、`state=open`、未合并且没有吸收宿主或管理员目标的目标;询问沿用 7 天过期,不进入 G1。材料是目标正文、涉及的人、截止时间、创建时间、来源消息(目标的出处)和候选消息。候选消息须晚于目标创建(管理员重开过则只取重开之后的新消息),不是目标自己的来源,所在入口的可见范围不窄于目标;发送者是涉及的人(为空时取来源消息中不是角色的发送者),或者是“我”而且发在这个目标的对话里(目标所在入口,或目标创建之后涉及的人发过言的入口)。迁移 039 为此在 messages 上加了 `(sender_subject_id, entry_id, id)` 索引。别处无关入口里我的发言既不进候选,也不改变指纹。最多 30 条:先固定保留最新的 10 条(不论入口和关键词),其余按目标入口且含关键词、目标入口或含关键词、最近的顺序补足。没有候选消息不调用模型。目标按最久未复核优先处理(从未复核的最先,其余按本次运行之前最后一次判断的时间升序),因调用上限没轮到的目标下次排在前面;本次维护中刚被 G2 合并进来的存续目标留到下一次维护再复核,以便管理员仍可撤销那次合并。提示词 `goal_review_auto_v1.md`,走 `chat` 用途(purpose `goal_auto_review`),只输出 `{"decision","evidence","reason"}`。代码核对:判断不是 open、依据非空且都在材料内;目标自己的来源(包括合并时继承的)一律不算依据,合格依据只能来自候选消息,其中至少一条晚于目标创建、发送者是涉及的人或“我”、类型是 message/self_output/action_result;目标仍为内部、未结束、修订未变。通过才置为完成或放弃,`closed_by=maintenance`,取消未取走的提醒,记目标快照(动作 `auto_review`,原因 `auto_completed`/`auto_abandoned`)和操作记录。每次判断写一行 `goal_auto_reviews`(迁移 039),指纹为“目标修订号+发送者合格的最新消息 ID”,指纹不变不再调用模型,有新的合格消息就重判。超时、限流、暂停失败不记指纹,下次再试;输出无效或核对失败保持未结束并记固定原因。模型的文字理由不保存。自动关闭时,逐条通过核对的合格依据消息记在 `goal_auto_review_evidence`,30 天消息清理不会删除它们;一同被引用的不合格消息不受保护,管理员重开(否决这次关闭)后解除这次关闭的保护。管理员可在目标详情、目标页“每日维护自动处理的目标”和维护报告中查看依据消息并重开(`POST /admin/api/goals/{id}/reopen`,带 `expected_revision`);重开后只有更晚的新消息才能作为依据,合并时存续目标取双方较大的重开水位,原依据不会再次关闭它。 + +**G2 可能重复补判**:对仍为 `possible` 的目标对,以较晚的一条为待判目标,调用与创建时去重相同的材料组装 `dedup_review_material`(`goal_dedup_probe` 驱动的创建路径用的也是它)和现行 D6 提示词,走 `goal_dedup_judge` 用途,每对预算 60 秒。较晚的一条按它自己的令牌范围组材料和合并,与创建时复核一致。只有宿主目标作为被并入的一方(较晚的一条本身,或任何已被合并进它的宿主目标)、要并进它令牌范围外的目标时才拦:不调用模型、不合并,保留可能重复标记,记 `kept`/`host_scope_not_merged`,按指纹不再重复处理;内部目标并进宿主目标照创建时规则判断。只有创建时规则会合并到这一对中较早的一方(唯一 same、无其他歧义、未超限、无序列冲突)才合并,走现有 `_merge`;含管理员创建的目标时不合并,撤销标记并记录。判为不重复撤销标记;不确定或仍有歧义保留标记。完成的判断记 `goal_duplicate_judgments`,按指纹(全部观察到的目标修订和最新来源消息)不再重判;超时或失败保留标记,下次再试;目标去重判断关闭时不运行。已知限制:指纹包含全部观察到的候选目标修订号,其中任一变化都会让 uncertain 的一对再判一次,重判可能偏多。写回只核对修订号,维护开始后才到的消息本次看不到,靠管理员重开兜底。 + +自动合并可以撤销:`POST /admin/api/goal-duplicate-judgments/{id}/undo`(管理员会话、CSRF,带存续目标的 `expected_revision`)只对每日维护的自动合并有效。合并时记下存续目标的截止时间、提前量、重开水位、来源、涉及人、承诺记忆、依据标注,以及重定向、可能重复关系和待取提醒的 ID(不含正文);撤销时存续目标在合并后被修改过(包括管理员编辑等任何修订)返回 409;否则恢复被并入目标,存续目标退回合并前的字段和关系。提醒按一条简单规则处理(宁可多发一次,不丢):已有提醒记录一律不改挂、不改正文、不恢复,已取走的不动;取消两边当前所有待取的提醒,两边都按还原后的字段重排计划,各自按当前所处阶段补发至多一条新提醒(新 ID):临近时刻已过且未到截止发临近提醒,已过截止时过期提醒开启则发过期提醒并排次日计划,关闭则发一条“目标已到期”提醒、不排次日计划,临近时刻未到则交给新计划。宿主可能因此对同一阶段多收到一条。这一对记为驳回,以后不再自动合并。目标页、目标详情和维护报告在自动合并项上提供撤销。 + +每次维护 G1 最多调用 100 次、G2 最多 50 次,超出的记“预算用尽”,留到下次。维护报告的 `goal_auto_review`/`goal_duplicate_review` 汇总调用、关闭、保持、无候选、未变化、预算用尽和停止原因(中断续跑时累加),并逐项列出关闭、合并、撤销标记及依据;`GET /admin/api/goal-auto-actions` 分页列出自动处理记录。宿主接口的目标字段不变。 + +评测:`uv run python evals/goal_review_eval/run.py --corpus --out <仓库外目录>`,默认语料是冻结的公开 dev 集 `evals/goal_review_v1.json`。每例隔离数据库、固定时钟,原样写入入口、人物、消息和目标后跑真实的目标阶段(只开 G1);`expected`、`reason`、`category` 在写入前剔除,全部案例跑完后才计分。确定性计分包括误关闭(应 open 却被关闭,或完成与放弃方向相反)、宿主/管理员目标被关闭数、识别率(应关闭案例中状态正确的比例)和按类别明细;`call_counts` 统计超时、429、截断、无效调用和执行错误,`valid` 要求完整、无超时/429/截断/执行错误且没有失败的复核。出现超时、429 或复核失败即停止,整次无效,换新目录重跑。`--fake completed|abandoned|open|invalid|timeout` 只检查管线,不读模型配置、不访问网络。G2 用现有 `evals/goal_dedup_probe/run.py --method D6 --budget-seconds 60` 测量。 + ## M2 人物与身份联系(后端) 人物管理接口沿用管理员会话、CSRF、JSON Content-Type 和本机 Host 校验;人物页提供列表、详情、否认、确认合并和别名管理,有待确认联系时导航和试用页会提示。 diff --git a/evals/goal_review_eval/goal_review_scoring.py b/evals/goal_review_eval/goal_review_scoring.py new file mode 100644 index 00000000..a3156e7d --- /dev/null +++ b/evals/goal_review_eval/goal_review_scoring.py @@ -0,0 +1,72 @@ +"""Deterministic G1 scoring; reads labels only after every case has run.""" +from __future__ import annotations + +from collections import Counter + +CLOSED = ('completed', 'abandoned') +# Completed model refusals / malformed output are quality observations; the goal +# simply stays open. Anything else in the call ledger is an execution failure. +QUALITY_CALL_RESULTS = {'success', 'invalid_output', 'item_error', 'content_rejection'} + + +def ratio(numerator, denominator): + return {'numerator': numerator, 'denominator': denominator, + 'rate': numerator / denominator if denominator else None} + + +def score_case(case, result): + """result: {'state', 'evidence_keys', 'origin', ...} from the product database.""" + expected = case['expected']['state'] + actual = result['state'] + false_close = (expected == 'open' and actual != 'open') or ( + expected in CLOSED and actual in CLOSED and actual != expected) + external = case['goal']['origin'] in ('host', 'admin') + row = {'case_id': case['id'], 'category': case['category'], 'origin': case['goal']['origin'], + 'expected': expected, 'actual': actual, 'correct': actual == expected, + 'false_close': false_close, 'external_closed': external and actual != 'open', + 'actual_evidence': result.get('evidence_keys', []), + 'expected_evidence': case['expected'].get('evidence', []), + 'review_reason': result.get('review_reason'), 'model_calls': result.get('model_calls', 0)} + if actual in CLOSED and row['expected_evidence']: + row['evidence_overlap'] = sorted(set(row['actual_evidence']) & set(row['expected_evidence'])) + return row + + +def call_counts(calls): + """Same validity vocabulary as the other evaluations: timeouts, 429, truncation, invalid calls.""" + return {'calls': len(calls), + 'timeouts': sum(bool(c.get('timed_out')) for c in calls), + 'rate_limited': sum(c.get('status_code') == 429 for c in calls), + 'truncated': sum(c.get('finish_reason') not in (None, 'stop') for c in calls), + 'invalid_calls': sum(c.get('result_category') in ('invalid_output', 'item_error') for c in calls), + 'execution_errors': sum(c.get('result_category') not in QUALITY_CALL_RESULTS for c in calls)} + + +def report(rows, calls, *, expected_cases, failures=()): + """Gates: false closures 0, host/admin closures 0, recognition >= 80%.""" + should_close = [r for r in rows if r['expected'] in CLOSED] + categories = {} + for name in sorted({r['category'] for r in rows}): + members = [r for r in rows if r['category'] == name] + categories[name] = {'cases': len(members), 'correct': sum(r['correct'] for r in members), + 'false_closures': sum(r['false_close'] for r in members), + 'actual': dict(Counter(r['actual'] for r in members))} + counts = call_counts(calls) + complete = len(rows) == expected_cases + valid = (complete and not failures and counts['timeouts'] == 0 and counts['rate_limited'] == 0 + and counts['truncated'] == 0 and counts['execution_errors'] == 0) + result = {'cases': len(rows), 'complete': complete, 'valid': valid, 'call_counts': counts, + 'review_failures': list(failures), + 'false_closures': {'count': sum(r['false_close'] for r in rows), + 'cases': [r['case_id'] for r in rows if r['false_close']]}, + 'external_closed': {'count': sum(r['external_closed'] for r in rows), + 'cases': [r['case_id'] for r in rows if r['external_closed']]}, + 'recognition': ratio(sum(r['correct'] for r in should_close), len(should_close)), + 'accuracy': ratio(sum(r['correct'] for r in rows), len(rows)), + 'categories': categories, 'results': rows} + result['gates'] = { + 'false_closures_zero': result['false_closures']['count'] == 0, + 'external_closed_zero': result['external_closed']['count'] == 0, + 'recognition_at_least_80_percent': (result['recognition']['rate'] is not None + and result['recognition']['rate'] >= .8)} + return result diff --git a/evals/goal_review_eval/run.py b/evals/goal_review_eval/run.py new file mode 100644 index 00000000..5a57acf5 --- /dev/null +++ b/evals/goal_review_eval/run.py @@ -0,0 +1,298 @@ +"""Goal auto review (G1) evaluation with deterministic scoring; no model judging. + + uv run python evals/goal_review_eval/run.py --out /absolute/external/run + uv run python evals/goal_review_eval/run.py --corpus /absolute/corpus.json --out /absolute/external/run + uv run python evals/goal_review_eval/run.py --fake completed --out /absolute/external/fake + +Each case gets its own SQLite database and a fixed clock at case.now. Entries, +subjects, messages up to now and one goal are installed literally; then the real +daily-maintenance goal phase runs with only G1 enabled (no consolidation, +persona, goal deduplication or G2). Only product material reaches the model: +expected, reason and category are stripped before seeding and read by +goal_review_scoring.py after every case ran. A real run reads IRIS_TEST_MODELS; --fake uses an +httpx MockTransport, reads no configuration and cannot reach the network +(simulation=true, never a gate result). A timeout, 429 or failed review stops +the run with valid=false; rerun into a fresh directory, never splice runs. +Databases are deleted after extraction unless --keep-databases. +""" +from __future__ import annotations + +import argparse +import copy +import hashlib +import json +import shutil +import sys +from datetime import datetime, timezone +from pathlib import Path + +import httpx + +ROOT = Path(__file__).resolve().parents[2] +sys.path.insert(0, str(Path(__file__).resolve().parent)) +from goal_review_scoring import report, score_case # noqa: E402 + +from iris.db import Store # noqa: E402 +from iris.goals import (AUTO_REVIEW_KEY, AUTO_REVIEW_PROMPT_VERSION, AUTO_REVIEW_PURPOSE, # noqa: E402 + DUPLICATE_AUTO_KEY, _deadline) +from iris.maintenance import Maintenance, PHASES # noqa: E402 +from iris.model_health import ModelHealth # noqa: E402 +from iris.models import Gateway, ModelConfig, ModelError, load_test_models # noqa: E402 +from iris.state import role_zone # noqa: E402 + +PRODUCT_FIELDS = ('now', 'entries', 'subjects', 'goal', 'messages') +LABEL_FIELDS = ('expected', 'reason', 'category') +FAKE_MODES = ('completed', 'abandoned', 'open', 'invalid', 'timeout') +PLATFORM = 'goal-review-eval' + + +def save(path, value): + path.parent.mkdir(parents=True, exist_ok=True) + path.write_text(json.dumps(value, ensure_ascii=False, indent=2) + '\n', encoding='utf-8') + + +def utc(value): + stamp = datetime.fromisoformat(value) + if stamp.tzinfo is None: + raise ValueError('timestamps require a timezone') + return stamp.astimezone(timezone.utc) + + +def validate_corpus(corpus): + if corpus.get('format_version') != 1 or not isinstance(corpus.get('cases'), list) or not corpus['cases']: + raise ValueError('unsupported corpus format') + ids = [case['id'] for case in corpus['cases']] + if len(set(ids)) != len(ids): + raise ValueError('duplicate case id') + for case in corpus['cases']: + utc(case['now']) + keys = [m['key'] for m in case['messages']] + if len(set(keys)) != len(keys): + raise ValueError(f'{case["id"]}: duplicate message key') + subjects = {s['id'] for s in case['subjects']} | {'self', 'scene'} + entries = {e['id'] for e in case['entries']} + goal = case['goal'] + if goal['kind'] not in ('normal', 'question') or goal['origin'] not in ('internal', 'host', 'admin'): + raise ValueError(f'{case["id"]}: invalid goal kind or origin') + utc(goal['created_at']) + if not set(goal['sources']) <= set(keys) or not set(goal['people']) <= subjects - {'self', 'scene'}: + raise ValueError(f'{case["id"]}: goal refers to an unknown message or person') + if goal.get('entry_id') is not None and goal['entry_id'] not in entries: + raise ValueError(f'{case["id"]}: unknown goal entry') + for message in case['messages']: + utc(message['at']) + if message['entry_id'] not in entries or message['sender'] not in subjects: + raise ValueError(f'{case["id"]}: message refers to an unknown entry or sender') + if message.get('quote_author') is not None and message['quote_author'] not in subjects: + raise ValueError(f'{case["id"]}: unknown quote author') + expected = case['expected'] + if expected['state'] not in ('open', 'completed', 'abandoned') or not set(expected.get('evidence', [])) <= set(keys): + raise ValueError(f'{case["id"]}: invalid expected label') + return corpus + + +def product_input(case): + """The only case fields that may reach the product (and so the model).""" + return {field: copy.deepcopy(case[field]) for field in PRODUCT_FIELDS} + + +def seed(store, material, corpus): + """Install facts literally; messages after now have not been received yet.""" + store.set_setting('timezone', corpus['timezone']) + store.set_setting('role_name', corpus.get('role_name', 'Iris')) + now = utc(material['now']) + keys, skipped = {}, [] + with store.write() as conn: + zone = role_zone(conn) + for entry in material['entries']: + conn.execute('INSERT INTO entries(id,name,platform,kind) VALUES(?,?,?,?)', + (entry['id'], entry.get('name') or entry['id'], PLATFORM, entry.get('kind', 'private'))) + for subject in material['subjects']: + conn.execute("INSERT INTO subjects(id,kind,name,created_at) VALUES(?,'person',?,?)", + (subject['id'], subject['name'], now.isoformat())) + conn.executemany('INSERT INTO subject_aliases(subject_id,alias) VALUES(?,?)', + [(subject['id'], alias) for alias in subject.get('aliases', [])]) + ids = {} + for number, message in enumerate(material['messages'], 1): + if utc(message['at']) > now: + skipped.append(message['key']) + continue + mid = conn.execute('''INSERT INTO messages(entry_id,kind,sender_subject_id,content,quote_author_subject_id, + quote_content,occurred_at,received_at,dedupe_key,learning_state) VALUES(?,?,?,?,?,?,?,?,?,'learned')''', + (message['entry_id'], message['kind'], message['sender'], message['content'], message.get('quote_author'), + message.get('quote_content'), message['at'], message['at'], f'case-{number}')).lastrowid + ids[message['key']] = mid + keys[mid] = message['key'] + goal = material['goal'] + created = utc(goal['created_at']).isoformat() + deadline = _deadline(goal.get('deadline'), zone) if goal.get('deadline') else None + gid = conn.execute('''INSERT INTO goals(content,kind,origin,state,deadline,deadline_at,entry_id,created_at, + updated_at,schedule_initialized) VALUES(?,?,?,?,?,?,?,?,?,1)''', + (goal['content'], goal['kind'], goal['origin'], goal.get('state', 'open'), deadline, deadline, + goal.get('entry_id'), created, created)).lastrowid + conn.executemany('INSERT INTO goal_people(goal_id,subject_id) VALUES(?,?)', [(gid, p) for p in goal['people']]) + conn.executemany('INSERT INTO goal_sources(goal_id,message_id) VALUES(?,?)', + [(gid, ids[key]) for key in goal['sources'] if key in ids]) + return gid, keys, skipped + + +class RecordingGateway(Gateway): + """Keeps the chat requests and completion content; never reasoning or headers.""" + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.records = [] + + def _call(self, kind, purpose, payload, **kwargs): + if kind != 'chat': + return super()._call(kind, purpose, payload, **kwargs) + record = {'purpose': purpose, 'messages': payload.get('messages')} + self.records.append(record) + try: + result = super()._call(kind, purpose, payload, **kwargs) + choice = (result.get('choices') or [{}])[0] + record.update(content=(choice.get('message') or {}).get('content'), finish_reason=choice.get('finish_reason'), + usage=result.get('usage')) + return result + except ModelError as exc: + record.update(error=exc.category, reason=exc.reason) + raise + + +def fake_transport(mode): + """Offline plumbing check: ignores labels and cannot access the network.""" + def respond(request): + if mode == 'timeout': + raise httpx.ReadTimeout('simulated goal review timeout', request=request) + content = 'invalid unit response' + if mode != 'invalid': + material = json.loads(json.loads(request.content)['messages'][-1]['content']) + evidence = [] if mode == 'open' else [material['messages'][-1]['id']] + content = json.dumps({'decision': mode, 'evidence': evidence, 'reason': 'fake'}) + return httpx.Response(200, json={'choices': [{'message': {'content': content}, 'finish_reason': 'stop'}], + 'usage': {'prompt_tokens': 1, 'completion_tokens': 1}}) + return httpx.MockTransport(respond) + + +def run_case(material, corpus, directory, configs, *, fake=None, keep_database=False): + directory.mkdir(parents=True) + path = directory / 'iris.db' + store = Store(path) + gateway = client = None + now = utc(material['now']) + clock = lambda: now # noqa: E731 + try: + gid, keys, skipped = seed(store, material, corpus) + store.set_setting('consolidation', {'enabled': False}) + store.set_setting('scheduled_backup', {'enabled': False}) + store.set_setting('goal_dedup_judge', {'enabled': False}) + store.set_setting('goals', {AUTO_REVIEW_KEY: True, DUPLICATE_AUTO_KEY: False}) + client = httpx.Client(transport=fake_transport(fake)) if fake else None + gateway = RecordingGateway(configs, store, health=ModelHealth(store, configs, clock=clock), clock=clock, + client=client, **({'sleeper': lambda seconds: None} if fake else {})) + engine = Maintenance(store, gateway=gateway, clock=clock) + run_id = engine.request(trigger='manual') + with store.write() as conn: + conn.execute('UPDATE maintenance_runs SET phase=? WHERE id=?', (PHASES.index('goals'), run_id)) + engine.run(run_id) + maintenance = engine.report(run_id) + with store.read() as conn: + goal = dict(conn.execute('SELECT id,origin,kind,state,closed_by,closed_at,revision FROM goals WHERE id=?', + (gid,)).fetchone()) + reviews = [dict(r) for r in conn.execute('SELECT * FROM goal_auto_reviews WHERE goal_id=? ORDER BY id', (gid,))] + items = [dict(r) for r in conn.execute( + "SELECT item_key,object_id,outcome,reason,details_json FROM maintenance_items WHERE run_id=? AND phase='goals'", + (run_id,))] + calls = [dict(r) for r in conn.execute('''SELECT purpose,model,status_code,result_category,timed_out, + finish_reason,reasoning_effort,prompt_tokens,completion_tokens,reasoning_tokens,duration_ms + FROM model_calls ORDER BY id''')] + review = reviews[-1] if reviews else None + evidence = json.loads(review['evidence_json']) if review else [] + result = {'state': goal['state'], 'closed_by': goal['closed_by'], 'origin': goal['origin'], + 'review_reason': review['reason'] if review else None, + 'review_decision': review['decision'] if review else None, + 'evidence_keys': [keys.get(mid, f'#{mid}') for mid in evidence], + 'failed_items': [i['reason'] for i in items if i['outcome'] == 'failed'], + 'skipped_future_messages': skipped, 'model_calls': len(calls), + 'goal_auto_review': maintenance.get('goal_auto_review')} + save(directory / 'result.json', {**result, 'goal': goal, 'reviews': reviews, 'items': items, 'calls': calls}) + save(directory / 'requests.json', gateway.records) + return result, calls + finally: + if gateway: + gateway.close() + if client: + client.close() + store.close() + if not keep_database: + for candidate in directory.glob('iris.db*'): + if candidate.is_dir(): + shutil.rmtree(candidate, ignore_errors=True) + else: + candidate.unlink(missing_ok=True) + + +def source_sha256(): + paths = sorted([*ROOT.joinpath('src/iris').rglob('*.py'), *ROOT.joinpath('src/iris').rglob('*.sql'), + *ROOT.joinpath('src/iris').rglob('*.md')]) + return hashlib.sha256(b''.join(p.relative_to(ROOT).as_posix().encode() + p.read_bytes() for p in paths)).hexdigest() + + +def main(argv=None): + parser = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter) + parser.add_argument('--corpus', type=Path, default=ROOT / 'evals/goal_review_v1.json') + parser.add_argument('--out', type=Path, required=True) + parser.add_argument('--fake', choices=FAKE_MODES, + help='offline transport self-check; no configuration read or network calls; not a gate result') + parser.add_argument('--keep-databases', action='store_true') + args = parser.parse_args(argv) + out = args.out.resolve() + if out.is_relative_to(ROOT): + parser.error('--out must be outside the repository') + corpus_bytes = args.corpus.read_bytes() + try: + corpus = validate_corpus(json.loads(corpus_bytes)) + except (ValueError, KeyError, TypeError) as exc: + parser.error(f'invalid corpus: {exc}') + out.mkdir(parents=True, exist_ok=False) + if args.fake: + configs = {'chat': ModelConfig('https://goal-review.invalid/v1', '', 'fake-goal-review')} + else: + configs = {'chat': load_test_models()['chat']} + prompt = ROOT / 'src/iris/prompts' / f'{AUTO_REVIEW_PROMPT_VERSION}.md' + metadata = {'evaluation': 'goal_review', 'format_version': 1, 'corpus_name': corpus.get('name'), + 'corpus_sha256': hashlib.sha256(corpus_bytes).hexdigest(), 'expected_cases': len(corpus['cases']), + 'timezone': corpus['timezone'], 'simulation': bool(args.fake), 'fake_mode': args.fake, + 'purpose': AUTO_REVIEW_PURPOSE, 'prompt_version': AUTO_REVIEW_PROMPT_VERSION, + 'prompt_sha256': hashlib.sha256(prompt.read_bytes()).hexdigest(), + 'runner_sha256': hashlib.sha256(Path(__file__).read_bytes()).hexdigest(), + 'scoring_sha256': hashlib.sha256(Path(__file__).with_name('goal_review_scoring.py').read_bytes()).hexdigest(), + 'source_sha256': source_sha256(), 'model': configs['chat'].model, + 'reasoning_effort': configs['chat'].reasoning_effort} + save(out / 'metadata.json', metadata) + results, rows, calls, failures = {}, [], [], [] + for case in corpus['cases']: + result, case_calls = run_case(product_input(case), corpus, out / 'cases' / case['id'], configs, + fake=args.fake, keep_database=args.keep_databases) + results[case['id']] = result + calls.extend(case_calls) + print(json.dumps({'case': case['id'], 'state': result['state'], 'calls': len(case_calls)}, ensure_ascii=False), + flush=True) + interrupted = [c for c in case_calls if c['timed_out'] or c['status_code'] == 429] + if result['failed_items']: + failures.append({'case_id': case['id'], 'reasons': result['failed_items']}) + if interrupted or result['failed_items']: + break # Preserve the invalid run; a retry must use a fresh output directory. + # Labels are read only here, after every executed case. + for case in corpus['cases']: + if case['id'] in results: + rows.append(score_case(case, results[case['id']])) + summary = {**metadata, **report(rows, calls, expected_cases=len(corpus['cases']), failures=failures)} + save(out / 'report.json', summary) + print(json.dumps({k: v for k, v in summary.items() if k not in ('results', 'categories')}, ensure_ascii=False), + flush=True) + return 0 if summary['valid'] else 2 + + +if __name__ == '__main__': + raise SystemExit(main()) diff --git a/frontend/src/GoalAutoReview.test.tsx b/frontend/src/GoalAutoReview.test.tsx new file mode 100644 index 00000000..bd3fe627 --- /dev/null +++ b/frontend/src/GoalAutoReview.test.tsx @@ -0,0 +1,468 @@ +import { render, screen, waitFor, within } from "@testing-library/react"; +import userEvent from "@testing-library/user-event"; +import { afterEach, beforeEach, expect, test, vi } from "vitest"; +import GoalDetails from "./GoalDetails"; +import { GoalAutoActions } from "./GoalReview"; +import { GoalSettings } from "./RuntimeSettings"; +import { MaintenanceReportDialog } from "./Maintenance"; +import { goalCatalog, goalDetailFixture } from "./goal-fixtures"; +import { response } from "./lifecycle-fixtures"; +import { setCSRF } from "./api"; +import type { GoalAutoAction, MaintenanceReport } from "./types"; + +const stamp = "2026-11-03T03:00:00+08:00"; +const evidence = { + id: 77, + missing: false, + entry_id: "trial-a", + entry_name: "试用群聊 A", + kind: "message", + sender_subject_id: "lin", + sender_name: "小林", + content: "周五已经联系过了,事情办完了。", + occurred_at: stamp, +}; +const closure = { + id: 5, + goal_id: 10, + goal_revision: 4, + latest_message_id: 77, + run_id: 3, + decision: "completed" as const, + outcome: "closed" as const, + reason: "auto_completed", + prompt_version: "goal_review_auto_v1", + revision_after: 5, + evidence: [77], + created_at: stamp, + evidence_messages: [evidence], +}; +let requests: { url: string; init?: RequestInit; body: any }[]; +let detail: any; +let actions: GoalAutoAction[]; +let conflict: boolean; +beforeEach(() => { + setCSRF("goal-auto-test-csrf"); + requests = []; + conflict = false; + detail = { + ...structuredClone(goalDetailFixture), + state: "completed", + closed_by: "maintenance", + closed_at: stamp, + revision: 5, + possible_duplicate: false, + possible_duplicate_ids: [], + auto_closed: true, + auto_review: { + closure, + reopenable: true, + reviews: [closure], + duplicate_judgments: [ + { + id: 2, + goal_a: 10, + goal_b: 12, + run_id: 3, + method: "D6", + verdict: "different", + outcome: "separated", + reason: "auto_different", + target_id: null, + created_at: stamp, + undoable: false, + }, + { + id: 4, + goal_a: 10, + goal_b: 13, + run_id: 3, + method: "D6", + verdict: "same", + outcome: "merged", + reason: "auto_merged", + target_id: 10, + created_at: stamp, + undoable: true, + source_id: 13, + target_revision: 5, + }, + ], + }, + }; + actions = [ + { + ...closure, + type: "review", + goal: { + id: 10, + content: "周五联系小林", + state: "completed", + revision: 5, + merged_into: null, + }, + current: true, + reopenable: true, + evidence_messages: [evidence], + }, + { + type: "duplicate", + id: 3, + goal_a: 20, + goal_b: 21, + run_id: 3, + method: "D6", + verdict: "same", + outcome: "merged", + reason: "auto_merged", + target_id: 20, + created_at: stamp, + undoable: true, + undone_at: null, + source_id: 21, + target_revision: 4, + goals: [ + { + id: 20, + content: "整理操作手册", + state: "open", + origin: "internal", + merged_into: null, + revision: 3, + }, + { + id: 21, + content: "整理操作说明手册", + state: "open", + origin: "internal", + merged_into: 20, + revision: 2, + }, + ], + }, + ]; + vi.stubGlobal( + "fetch", + vi.fn(async (url: string, init?: RequestInit) => { + const body = JSON.parse(String(init?.body || "{}")); + requests.push({ url, init, body }); + if (url.endsWith("/undo")) { + if (conflict) + return response( + { error: { code: "revision_conflict", message: "目标已变化" } }, + 409, + ); + return response(detail); + } + if (url.endsWith("/reopen")) { + if (conflict) + return response( + { error: { code: "revision_conflict", message: "目标已变化" } }, + 409, + ); + detail = { + ...detail, + state: "open", + closed_by: null, + closed_at: null, + revision: 6, + auto_closed: false, + auto_review: { + ...detail.auto_review, + closure: null, + reopenable: false, + }, + }; + return response(detail); + } + if (url === "/admin/api/goals/10") return response(detail); + if (url.startsWith("/admin/api/goal-auto-actions")) + return response({ items: actions, total: 2, limit: 10, offset: 0 }); + if ( + url.includes("/sources?") || + url.includes("/revisions?") || + url.includes("/operations?") || + url.includes("/notifications?") + ) + return response({ items: [], total: 0, limit: 30, offset: 0 }); + throw new Error(`Unexpected route: ${url}`); + }), + ); +}); +afterEach(() => vi.unstubAllGlobals()); + +test("目标详情列出自动完成的依据消息,重开带修订号与 CSRF", async () => { + render( + , + ); + const panel = await screen.findByRole("region", { name: "每日维护自动复核" }); + expect( + within(panel).getByText("周五已经联系过了,事情办完了。"), + ).toBeVisible(); + expect(within(panel).getAllByText(/依据明确,自动完成/)[0]).toBeVisible(); + expect(screen.getByText("维护自动完成")).toBeVisible(); + await userEvent.click(within(panel).getByText(/可能重复补判/)); + expect( + within(panel).getByText(/判为不重复,已撤销可能重复标记/), + ).toBeVisible(); + await userEvent.click( + within(panel).getByRole("button", { name: "重开目标" }), + ); + expect(screen.getByText(/不会凭原来的依据再次关闭它/)).toBeVisible(); + await userEvent.click(screen.getByRole("button", { name: "确认重开" })); + expect(await screen.findByText("目标已重开")).toBeVisible(); + const request = requests.find((r) => r.url.endsWith("/reopen"))!; + expect(request.url).toBe("/admin/api/goals/10/reopen"); + expect(request.init?.method).toBe("POST"); + expect(request.body).toEqual({ expected_revision: 5 }); + expect((request.init?.headers as any)["X-Iris-CSRF"]).toBe( + "goal-auto-test-csrf", + ); + await waitFor(() => + expect( + screen.queryByRole("button", { name: "重开目标" }), + ).not.toBeInTheDocument(), + ); +}); + +test("目标页的自动处理列表在展开前不请求,展开后列出完成与合并并可重开", async () => { + const openGoal = vi.fn(), + onChange = vi.fn(); + render(); + expect(requests).toEqual([]); + await userEvent.click( + screen.getByRole("button", { name: "每日维护自动处理的目标" }), + ); + expect(await screen.findByText("周五联系小林")).toBeVisible(); + expect(screen.getByText("#21:整理操作说明手册")).toBeVisible(); + expect(screen.getByText(/判为重复,已合并到较早的目标/)).toBeVisible(); + await userEvent.click(screen.getByRole("button", { name: "查看目标 #20" })); + expect(openGoal).toHaveBeenCalledWith(20); + conflict = true; + await userEvent.click(screen.getByRole("button", { name: "重开目标 #10" })); + await userEvent.click(screen.getByRole("button", { name: "确认重开" })); + expect(await screen.findByText(/目标已变化,请刷新后重新核对/)).toBeVisible(); + conflict = false; + await userEvent.click(screen.getByRole("button", { name: "确认重开" })); + expect(await screen.findByText("目标 #10 已重开")).toBeVisible(); + expect(onChange).toHaveBeenCalled(); + expect( + requests.filter((r) => r.url.endsWith("/reopen")).at(-1)!.body, + ).toEqual({ expected_revision: 5 }); +}); + +test("两个自动复核开关默认关闭,只保存改动的开关", async () => { + const save = vi.fn(async (changes: Record) => ({ + default_reminder_minutes: 60, + overdue_reminders: true, + ...changes, + })); + render( + , + ); + const review = screen.getByLabelText("每日维护按依据自动完成或放弃内部目标"); + const duplicates = screen.getByLabelText("每日维护补判“可能重复”的目标"); + expect(review).not.toBeChecked(); + expect(duplicates).not.toBeChecked(); + await userEvent.click(review); + await userEvent.click( + screen.getByRole("button", { name: "保存目标与提醒设置" }), + ); + expect(save).toHaveBeenCalledWith({ goal_auto_review_enabled: true }); +}); + +test("维护报告列出自动复核结果和依据,可直接重开", async () => { + const report: MaintenanceReport = { + id: 3, + trigger: "scheduled", + state: "completed", + phase: 10, + created_at: stamp, + finished_at: stamp, + timezone: "Asia/Shanghai", + summary: { goals_auto_completed: { count: 1, object_ids: [10] } }, + goal_auto_review: { enabled: true, calls: 2, closed: 1, kept_open: 1 }, + goal_duplicate_review: { enabled: false, skip_reason: "disabled" }, + items: [ + { + phase: "goals", + item_key: "auto_review:10", + memory_id: null, + object_id: 10, + outcome: "goals_auto_completed", + reason: "auto_completed", + details: { kind: "auto_review", evidence: [77], revision_after: 5 }, + created_at: stamp, + }, + { + phase: "goals", + item_key: "auto_review:11", + memory_id: null, + object_id: 11, + outcome: "goal_review_kept", + reason: "evidence_not_after_creation", + details: { kind: "auto_review", evidence: [5] }, + created_at: stamp, + }, + ], + }; + vi.stubGlobal( + "fetch", + vi.fn(async (url: string, init?: RequestInit) => { + requests.push({ + url, + init, + body: JSON.parse(String(init?.body || "{}")), + }); + if (url === "/admin/api/maintenance/3") return response(report); + if (url === "/admin/api/goals/10/reopen") return response(detail); + throw new Error(`Unexpected route: ${url}`); + }), + ); + render( + , + ); + const section = await screen.findByRole("region", { name: "目标自动复核" }); + expect( + within(section).getByText(/模型调用 2 · 关闭 1 · 保持未结束 1/), + ).toBeVisible(); + expect(within(section).getByText(/可能重复补判:本次未开启/)).toBeVisible(); + expect(within(section).getByText("依据消息:#77")).toBeVisible(); + expect( + within(section).getByRole("link", { name: "查看目标 #10" }), + ).toHaveAttribute("href", "#/state?tab=goals&id=10"); + const kept = screen.getByRole("region", { name: "目标复核保持不变" }); + expect(within(kept).getByText(/依据不晚于目标创建或重开/)).toBeVisible(); + await userEvent.click( + within(section).getByRole("button", { name: "重开目标 #10" }), + ); + expect(await within(section).findByText("已重开")).toBeVisible(); + expect(requests.find((r) => r.url.endsWith("/reopen"))!.body).toEqual({ + expected_revision: 5, + }); +}); + +test("自动合并可在目标页列表撤销,冲突时提示,成功后刷新", async () => { + const onChange = vi.fn(); + render(); + await userEvent.click( + screen.getByRole("button", { name: "每日维护自动处理的目标" }), + ); + await userEvent.click( + await screen.findByRole("button", { name: "撤销合并 #3" }), + ); + expect(screen.getByText(/撤销后这两条目标不再被自动合并/)).toBeVisible(); + conflict = true; + await userEvent.click(screen.getByRole("button", { name: "确认撤销合并" })); + expect( + await screen.findByText(/保留目标在合并后已变化,不能撤销/), + ).toBeVisible(); + conflict = false; + await userEvent.click(screen.getByRole("button", { name: "确认撤销合并" })); + expect( + await screen.findByText("已撤销目标 #20 与 #21 的自动合并"), + ).toBeVisible(); + const request = requests.filter((r) => r.url.endsWith("/undo")).at(-1)!; + expect(request.url).toBe("/admin/api/goal-duplicate-judgments/3/undo"); + expect(request.body).toEqual({ expected_revision: 4 }); + expect((request.init?.headers as any)["X-Iris-CSRF"]).toBe( + "goal-auto-test-csrf", + ); + expect(onChange).toHaveBeenCalled(); +}); + +test("目标详情的可能重复补判记录提供撤销合并,只对可撤销项显示", async () => { + render( + , + ); + const panel = await screen.findByRole("region", { name: "每日维护自动复核" }); + await userEvent.click(within(panel).getByText(/可能重复补判/)); + expect( + within(panel).queryByRole("button", { name: "撤销合并 #2" }), + ).not.toBeInTheDocument(); + await userEvent.click( + within(panel).getByRole("button", { name: "撤销合并 #4" }), + ); + await userEvent.click(screen.getByRole("button", { name: "确认撤销合并" })); + expect(await screen.findByText("已撤销自动合并")).toBeVisible(); + const request = requests.find((r) => r.url.endsWith("/undo"))!; + expect(request.url).toBe("/admin/api/goal-duplicate-judgments/4/undo"); + expect(request.body).toEqual({ expected_revision: 5 }); +}); + +test("维护报告的自动合并项可直接撤销,预算用尽单列", async () => { + const report: MaintenanceReport = { + id: 4, + trigger: "scheduled", + state: "completed", + phase: 10, + created_at: stamp, + finished_at: stamp, + timezone: "Asia/Shanghai", + summary: { goal_duplicates_merged: { count: 1, object_ids: [20] } }, + goal_auto_review: { enabled: true, calls: 100, budget_exhausted: 3 }, + goal_duplicate_review: { enabled: true, calls: 1, merged: 1 }, + items: [ + { + phase: "goals", + item_key: "duplicate:20:21", + memory_id: null, + object_id: 20, + outcome: "goal_duplicates_merged", + reason: "auto_merged", + details: { + kind: "duplicate", + goal_ids: [20, 21], + target_id: 20, + judgment_id: 3, + target_revision_after: 4, + }, + created_at: stamp, + }, + ], + }; + vi.stubGlobal( + "fetch", + vi.fn(async (url: string, init?: RequestInit) => { + requests.push({ + url, + init, + body: JSON.parse(String(init?.body || "{}")), + }); + if (url === "/admin/api/maintenance/4") return response(report); + if (url === "/admin/api/goal-duplicate-judgments/3/undo") + return response(detail); + throw new Error(`Unexpected route: ${url}`); + }), + ); + render( + , + ); + const section = await screen.findByRole("region", { name: "目标自动复核" }); + expect(within(section).getByText(/模型调用 100 · 预算用尽 3/)).toBeVisible(); + await userEvent.click( + within(section).getByRole("button", { name: "撤销合并 #3" }), + ); + expect(await within(section).findByText("已撤销合并")).toBeVisible(); + expect(requests.find((r) => r.url.endsWith("/undo"))!.body).toEqual({ + expected_revision: 4, + }); +}); diff --git a/frontend/src/GoalDetails.tsx b/frontend/src/GoalDetails.tsx index 8be5a3aa..955d12d1 100644 --- a/frontend/src/GoalDetails.tsx +++ b/frontend/src/GoalDetails.tsx @@ -1,4 +1,10 @@ -import { GoalBasisReview, GoalDedupReview } from "./GoalReview"; +import { + GoalAutoReviewPanel, + GoalBasisReview, + GoalDedupReview, + ReopenConfirm, + UndoMergeConfirm, +} from "./GoalReview"; import { useState } from "react"; import { api, ApiError, errorText, json, useData } from "./api"; import { Dialog, Notice, RoleTime, lifecycleLabel } from "./ui"; @@ -12,10 +18,12 @@ import type { GoalCatalog, GoalDetail, GoalBasisAnnotation, + GoalDuplicateJudgment, } from "./types"; type Action = - | { kind: "completed" | "abandoned"; goal: Goal } + | { kind: "completed" | "abandoned" | "reopen"; goal: Goal } + | { kind: "undo_merge"; goal: Goal; judgment: GoalDuplicateJudgment } | { kind: "merge"; goal: Goal; other: Goal } | { kind: "basis_clear"; goal: Goal; annotation: GoalBasisAnnotation }; function Duplicate({ @@ -140,7 +148,13 @@ export default function GoalDetails({ openGoal(result.id); } else setNotice( - path.endsWith("/dismiss") ? "已驳回可能重复关系" : "目标已更新", + path.endsWith("/dismiss") + ? "已驳回可能重复关系" + : path.endsWith("/reopen") + ? "目标已重开" + : path.endsWith("/undo") + ? "已撤销自动合并" + : "目标已更新", ); } catch (e) { setError(errorText(e)); @@ -275,6 +289,15 @@ export default function GoalDetails({ setAction({ kind: "basis_clear", goal, annotation }) } /> + setAction({ kind: "reopen", goal })} + undoMerge={(judgment) => + setAction({ kind: "undo_merge", goal, judgment }) + } + /> {!goal.merged_into && goal.possible_duplicate_ids.length > 0 && (
@@ -386,7 +409,37 @@ export default function GoalDetails({ /> )} - {action && ( + {action?.kind === "reopen" && ( + setAction(null)} + confirm={() => + void write( + `/goals/${id}/reopen`, + { expected_revision: action.goal.revision }, + "POST", + ) + } + /> + )} + {action?.kind === "undo_merge" && ( + setAction(null)} + confirm={() => + void write( + `/goal-duplicate-judgments/${action.judgment.id}/undo`, + { expected_revision: action.judgment.target_revision }, + "POST", + ) + } + /> + )} + {action && action.kind !== "reopen" && action.kind !== "undo_merge" && ( = { basis_clear: "清除依据标注", basis_review: "复核目标依据", dedup: "目标去重判断", + auto_review: "每日维护自动复核", + reopen: "重开目标", + merge_undo: "撤销自动合并", }; const revisionReasons: Record = { "goal created": "创建目标", @@ -127,6 +130,13 @@ const revisionReasons: Record = { "possible duplicate dismissed": "驳回可能重复关系", "goal basis reviewed": "复核目标依据", "basis warning cleared by administrator": "管理员清除依据标注", + "goal reopened by administrator": "管理员重开目标", + auto_completed: "每日维护按依据自动完成", + auto_abandoned: "每日维护按依据自动放弃", + auto_merged: "每日维护补判为重复并合并", + auto_different: "每日维护补判为不重复", + admin_not_merged: "补判为重复,含管理员目标未合并", + "automatic merge undone by administrator": "管理员撤销自动合并", }; const relationStates: Record = { possible: "可能重复", diff --git a/frontend/src/GoalReview.tsx b/frontend/src/GoalReview.tsx index 61399dbf..c55906c9 100644 --- a/frontend/src/GoalReview.tsx +++ b/frontend/src/GoalReview.tsx @@ -1,7 +1,17 @@ -import { Badge, RoleTime } from "./ui"; -import { dedupNames } from "./goal-ui"; +import { useState } from "react"; +import { api, ApiError, errorText, json, useData } from "./api"; +import { Badge, Dialog, Notice, Pagination, RoleTime } from "./ui"; +import { dedupNames, goalAutoReason, goalStates } from "./goal-ui"; import { consolidationReason } from "./consolidation-labels"; -import type { GoalBasisAnnotation, GoalDetail } from "./types"; +import type { + Goal, + GoalAutoAction, + GoalBasisAnnotation, + GoalDetail, + GoalDuplicateJudgment, + GoalEvidenceMessage, + Page, +} from "./types"; export function GoalBasisReview({ goal, @@ -130,3 +140,395 @@ export function GoalDedupReview({
); } + +const decisionNames: Record = { + completed: "完成", + abandoned: "放弃", + open: "未结束", + invalid: "输出无效", +}; +const outcomeNames: Record = { + merged: "已合并", + separated: "已撤销可能重复", + kept: "保留可能重复", +}; +export function UndoMergeConfirm({ + judgment, + busy, + error, + confirm, + cancel, +}: { + judgment: Pick; + busy: boolean; + error?: string; + confirm: () => void; + cancel: () => void; +}) { + const source = + judgment.target_id === judgment.goal_a ? judgment.goal_b : judgment.goal_a; + return ( + + {error && {error}} +
+

+ 恢复目标 #{source},保留目标 #{judgment.target_id} + 的截止时间、提前量、来源和提醒退回合并前的状态。 +

+

+ 保留目标在合并后被修改过时不能撤销。撤销后这两条目标不再被自动合并。 +

+
+ + +
+
+
+ ); +} +export function GoalEvidenceList({ + messages, +}: { + messages: GoalEvidenceMessage[]; +}) { + if (!messages.length) return

没有依据消息。

; + return ( +
    + {messages.map((m) => ( +
  • + {m.missing ? ( + + 消息 #{m.id}:{m.notice || "消息已清理或不可用"} + + ) : ( + <> + + 消息 #{m.id} · {m.sender_name || m.sender_subject_id} ·{" "} + {m.entry_name || m.entry_id} ·{" "} + + +

    {m.content}

    + + )} +
  • + ))} +
+ ); +} +export function GoalAutoReviewPanel({ + goal, + locked, + reopen, + undoMerge, + openGoal, +}: { + goal: GoalDetail; + locked: boolean; + reopen: () => void; + undoMerge: (judgment: GoalDuplicateJudgment) => void; + openGoal: (id: number) => void; +}) { + const auto = goal.auto_review; + if ( + !auto || + (!auto.closure && !auto.reviews.length && !auto.duplicate_judgments.length) + ) + return null; + return ( +
+

每日维护自动复核

+ {auto.closure && ( +
+

+ + 自动{decisionNames[auto.closure.decision]} + {" "} + {goalAutoReason(auto.closure.reason)} ·{" "} + +

+

依据消息

+ + {auto.reopenable && ( +
+ +
+ )} +
+ )} + {auto.reviews.length > 0 && ( +
+ 复核记录 · {auto.reviews.length} 条 + {auto.reviews.map((r) => ( +

+ · 修订 {r.goal_revision} · 判断 + {decisionNames[r.decision]} · {goalAutoReason(r.reason)} + {r.evidence.length > 0 && + ` · 依据消息 ${r.evidence.map((id) => "#" + id).join("、")}`} +

+ ))} +
+ )} + {auto.duplicate_judgments.length > 0 && ( +
+ 可能重复补判 · {auto.duplicate_judgments.length} 条 + {auto.duplicate_judgments.map((j) => { + const other = j.goal_a === goal.id ? j.goal_b : j.goal_a; + return ( +
+

+ · 与目标 #{other} ·{" "} + {outcomeNames[j.outcome]} · {goalAutoReason(j.reason)} +

+
+ + {j.undoable && ( + + )} + {j.undone_at && 已撤销} +
+
+ ); + })} +
+ )} +

+ 只有学习产生的普通目标会按依据自动完成或放弃;宿主和管理员创建的目标不会被自动关闭。重开后,只有更晚的新消息才可能让它再次被关闭。 +

+
+ ); +} +export function ReopenConfirm({ + goal, + busy, + error, + confirm, + cancel, +}: { + goal: Pick; + busy: boolean; + error?: string; + confirm: () => void; + cancel: () => void; +}) { + return ( + + {error && {error}} +
+

{goal.content}

+

+ 重开后目标恢复未结束并重新安排提醒。之后每日维护只会参考比这次重开更晚的消息,不会凭原来的依据再次关闭它。 +

+
+ + +
+
+
+ ); +} +export function GoalAutoActions({ + openGoal, + onChange, +}: { + openGoal: (id: number) => void; + onChange: () => void; +}) { + const [open, setOpen] = useState(false), + [offset, setOffset] = useState(0); + const list = useData>( + open ? `/goal-auto-actions?limit=10&offset=${offset}` : null, + ); + const [target, setTarget] = useState< + Extract["goal"] | null + >(null); + const [merge, setMerge] = useState | null>(null); + const [busy, setBusy] = useState(false), + [error, setError] = useState(""), + [notice, setNotice] = useState(""); + async function reopen() { + if (!target || busy) return; + setBusy(true); + setError(""); + try { + await api( + `/goals/${target.id}/reopen`, + json("POST", { expected_revision: target.revision }), + ); + setNotice(`目标 #${target.id} 已重开`); + setTarget(null); + list.refresh(); + onChange(); + } catch (e) { + setError( + e instanceof ApiError && e.status === 409 + ? "目标已变化,请刷新后重新核对。" + : errorText(e), + ); + } finally { + setBusy(false); + } + } + async function undo() { + if (!merge || busy) return; + setBusy(true); + setError(""); + try { + await api( + `/goal-duplicate-judgments/${merge.id}/undo`, + json("POST", { expected_revision: merge.target_revision }), + ); + setNotice(`已撤销目标 #${merge.goal_a} 与 #${merge.goal_b} 的自动合并`); + setMerge(null); + list.refresh(); + onChange(); + } catch (e) { + setError( + e instanceof ApiError && e.status === 409 + ? "保留目标在合并后已变化,不能撤销;请到目标页核对。" + : errorText(e), + ); + } finally { + setBusy(false); + } + } + return ( +
+ + {open && ( + <> +

+ 列出每日维护按依据自动完成、放弃的目标,以及可能重复补判后合并或撤销标记的目标。 +

+ {notice && {notice}} + {list.error && {list.error}} + {!list.data && !list.error &&

正在读取…

} + {list.data?.items.length === 0 && ( +

暂无自动处理记录。

+ )} + {list.data?.items.map((item) => + item.type === "review" ? ( +
+

+ + 自动{decisionNames[item.decision]} + {" "} + 目标 #{item.goal.id} · 当前{goalStates[item.goal.state]} ·{" "} + + {!item.current && " · 已不是当前状态"} +

+

{item.goal.content}

+ +
+ + {item.reopenable && ( + + )} +
+
+ ) : ( +
+

+ {outcomeNames[item.outcome]} 目标 # + {item.goal_a} 与 #{item.goal_b} ·{" "} + {goalAutoReason(item.reason)} ·{" "} + +

+ {item.goals.map((g) => ( +

+ #{g.id}:{g.content} +

+ ))} +
+ {item.goals.map((g) => ( + + ))} + {item.undoable && ( + + )} + {item.undone_at && 已撤销} +
+
+ ), + )} + {list.data && ( + + )} + + )} + {merge && ( + void undo()} + cancel={() => setMerge(null)} + /> + )} + {target && ( + void reopen()} + cancel={() => setTarget(null)} + /> + )} +
+ ); +} diff --git a/frontend/src/Goals.tsx b/frontend/src/Goals.tsx index cb1ac8ad..cb20876d 100644 --- a/frontend/src/Goals.tsx +++ b/frontend/src/Goals.tsx @@ -3,6 +3,7 @@ import { api, errorText, json, useData } from "./api"; import { Dialog, Empty, Notice, Pagination } from "./ui"; import GoalDetails from "./GoalDetails"; import GoalForm from "./GoalForm"; +import { GoalAutoActions } from "./GoalReview"; import { GoalBadges, GoalFacts, @@ -270,6 +271,7 @@ export default function Goals({ change={setOffset} /> )} + )} {creating && ( diff --git a/frontend/src/Maintenance.tsx b/frontend/src/Maintenance.tsx index c6452ba2..e34898ab 100644 --- a/frontend/src/Maintenance.tsx +++ b/frontend/src/Maintenance.tsx @@ -1,7 +1,8 @@ import { ConsolidationReportSections } from "./ConsolidationReport"; import { consolidationReason } from "./consolidation-labels"; import { useEffect, useState } from "react"; -import { api, errorText, json, useData } from "./api"; +import { api, ApiError, errorText, json, useData } from "./api"; +import { goalAutoReason, goalAutoReasons } from "./goal-ui"; import { Badge, Dialog, @@ -53,8 +54,20 @@ const countNames: Record = { persona_pending: "persona 待确认", person_summary: "人物摘要处理", goals_reviewed: "目标依据标注变化", + goals_auto_completed: "目标自动完成", + goals_auto_abandoned: "目标自动放弃", + goal_review_kept: "目标复核保持未结束", + goal_duplicates_merged: "重复目标合并", + goal_duplicates_separated: "撤销可能重复", + goal_duplicates_kept: "保留可能重复", model_calls: "模型调用", }; +const goalAutoOutcomes = [ + "goals_auto_completed", + "goals_auto_abandoned", + "goal_duplicates_merged", + "goal_duplicates_separated", +]; const reasonNames: Record = { pinned: "已置顶", revision_conflict: "修订冲突", @@ -188,7 +201,7 @@ export function MaintenancePanel({

手动维护也计一次衰减,并执行遗忘、到期删除、消息清理和批次重试。还会按本次设置调用模型进行整理与 persona - 与人物摘要更新,并复核目标依据。已有未完成的维护时继续该次运行。 + 与人物摘要更新,复核目标依据;目标自动复核和可能重复补判开启时也会执行。已有未完成的维护时继续该次运行。

+ ))} + {closed && + i.details.revision_after != null && + (done.includes(i.object_id) ? ( + 已重开 + ) : ( + + ))} +
+ {fullTime(i.created_at)} + + ); + })} + + ); +} diff --git a/frontend/src/Operations.tsx b/frontend/src/Operations.tsx index 9505124f..1ab6a31d 100644 --- a/frontend/src/Operations.tsx +++ b/frontend/src/Operations.tsx @@ -30,6 +30,10 @@ export const operationNames: Record = { consolidation_annotation_clear: "清除整理建议", goal_basis_review: "目标依据复核", goal_basis_clear: "清除目标依据标注", + goal_auto_review: "每日维护自动关闭目标", + goal_reopen: "重开目标", + goal_duplicate_auto_judged: "每日维护补判可能重复", + goal_merge_undo: "撤销自动合并", persona_settings_saved: "更新 persona 设置", persona_edit: "编辑发布 persona", persona_confirm: "确认 persona 候选", diff --git a/frontend/src/RuntimeSettings.tsx b/frontend/src/RuntimeSettings.tsx index 5491bb5e..5951dc81 100644 --- a/frontend/src/RuntimeSettings.tsx +++ b/frontend/src/RuntimeSettings.tsx @@ -185,6 +185,9 @@ export function GoalSettings({ const current = { ...value, internal_question_expiry_days: value.internal_question_expiry_days ?? 7, + goal_auto_review_enabled: value.goal_auto_review_enabled ?? false, + goal_duplicate_auto_judge_enabled: + value.goal_duplicate_auto_judge_enabled ?? false, }; const { draft, change, reset } = useSettingsDraft({ default_reminder_minutes: String(current.default_reminder_minutes), @@ -192,6 +195,9 @@ export function GoalSettings({ internal_question_expiry_days: String( current.internal_question_expiry_days, ), + goal_auto_review_enabled: current.goal_auto_review_enabled, + goal_duplicate_auto_judge_enabled: + current.goal_duplicate_auto_judge_enabled, }); const [error, setError] = useState(""), [saved, setSaved] = useState(false); @@ -224,6 +230,9 @@ export function GoalSettings({ default_reminder_minutes: minutes, overdue_reminders: draft.overdue_reminders, internal_question_expiry_days: days, + goal_auto_review_enabled: draft.goal_auto_review_enabled, + goal_duplicate_auto_judge_enabled: + draft.goal_duplicate_auto_judge_enabled, }); if (!Object.keys(changes).length) return; if (await save(changes)) { @@ -287,6 +296,33 @@ export function GoalSettings({

只作用于学习产生且仍未结束的询问,到期由每日维护自动结束。宿主或管理员创建的询问和普通目标不受影响。

+ +

+ 只处理学习产生、仍未结束的普通目标。模型给出依据消息,程序核对依据晚于目标创建、来自涉及的人或角色自己后才关闭;含糊或部分完成的说法保持未结束。宿主和管理员创建的目标不受影响,自动关闭的目标可以在目标页重开。默认关闭。 +

+ +

+ 使用目标去重判断模型,每对最多 60 + 秒:判为重复时合并到较早的一条(含管理员创建的目标时不合并、改为撤销标记),判为不重复时撤销标记,仍不确定时保留标记。默认关闭。 +

{(error || saveError) && {error || saveError}} {saved && 目标与提醒设置已保存} diff --git a/frontend/src/goal-ui.tsx b/frontend/src/goal-ui.tsx index 9b94086b..760cc926 100644 --- a/frontend/src/goal-ui.tsx +++ b/frontend/src/goal-ui.tsx @@ -44,6 +44,11 @@ export function GoalBadges({ goal }: { goal: Goal }) { ) : null} {goal.possible_duplicate && 可能重复} {goal.basis_needs_review && 依据可能不成立} + {goal.auto_closed && ( + + {goal.state === "abandoned" ? "维护自动放弃" : "维护自动完成"} + + )} {goal.merged_into && 已合并} ); @@ -125,3 +130,44 @@ export function GoalReceiptNotice({ ); } +export const goalAutoReasons: Record = { + auto_completed: "依据明确,自动完成", + auto_abandoned: "依据明确,自动放弃", + decision_open: "判断仍未结束", + invalid_output: "模型输出无效,保持未结束", + content_rejection: "模型拒绝处理,保持未结束", + evidence_empty: "未给出依据,保持未结束", + evidence_outside_material: "依据不在材料内,保持未结束", + evidence_only_sources: "依据只有目标自己的出处消息,保持未结束", + evidence_missing: "依据消息已不存在,保持未结束", + evidence_not_after_creation: "依据不晚于目标创建或重开,保持未结束", + evidence_sender_not_involved: "依据不是涉及的人或角色所说,保持未结束", + evidence_kind_not_allowed: "依据只有场景事件,保持未结束", + auto_merged: "判为重复,已合并到较早的目标", + auto_different: "判为不重复,已撤销可能重复标记", + admin_not_merged: "判为重复,但含管理员创建的目标,不合并并撤销标记", + host_scope_not_merged: + "宿主目标会被并入它令牌范围外的目标,不合并,保留可能重复标记", + uncertain: "仍不确定,保留可能重复标记", + same_ambiguous: "判为重复但还有其他歧义,保留标记", + partner_not_in_material: "对方不在去重材料中,保留标记", + partner_not_judged: "没有得到对方的判断,保留标记", + merge_blocked: "不满足合并条件,保留标记", + no_candidates: "没有新的候选消息,未调用模型", + already_judged: "目标与材料未变,未再次调用模型", + budget_exhausted: "预算用尽,留到下次", + merged_this_run: "本轮刚被自动合并,下次维护再复核", + revision_conflict: "目标在判断期间已变化,下次再处理", + dedup_judge_disabled: "目标去重判断已关闭", + review_pending: "创建时的去重复核尚未结束", + pair_changed: "可能重复关系已变化", + disabled: "开关已关闭", + timeout: "超时,下次再试", + rate_limited: "限流,下次再试", + paused: "模型用途已暂停", + usage_limit: "达到每日 token 上限,下次再试", + merge_undone: "自动合并已撤销", + unconfigured: "模型未配置", +}; +export const goalAutoReason = (value: string | null | undefined) => + value ? goalAutoReasons[value] || value : ""; diff --git a/frontend/src/types.ts b/frontend/src/types.ts index bfeecf46..ac1fec95 100644 --- a/frontend/src/types.ts +++ b/frontend/src/types.ts @@ -211,12 +211,80 @@ export type Goal = { possible_duplicate_ids: number[]; basis_needs_review?: boolean; basis_annotations?: GoalBasisAnnotation[]; + auto_closed?: boolean; }; export type GoalConfig = { internal_question_expiry_days?: number; default_reminder_minutes: number; overdue_reminders: boolean; + goal_auto_review_enabled?: boolean; + goal_duplicate_auto_judge_enabled?: boolean; }; +export type GoalEvidenceMessage = { + id: number; + missing: boolean; + notice?: string; + entry_id?: string; + entry_name?: string | null; + kind?: string; + sender_subject_id?: string; + sender_name?: string | null; + content?: string; + occurred_at?: string; +}; +export type GoalAutoReviewRecord = { + id: number; + goal_id: number; + goal_revision: number; + latest_message_id: number; + run_id: number | null; + decision: "completed" | "abandoned" | "open" | "invalid"; + outcome: "closed" | "kept_open"; + reason: string; + prompt_version: string; + revision_after: number | null; + evidence: number[]; + created_at: string; + evidence_messages?: GoalEvidenceMessage[]; +}; +export type GoalDuplicateJudgment = { + id: number; + goal_a: number; + goal_b: number; + run_id: number | null; + method: string; + verdict: "same" | "different" | "uncertain" | null; + outcome: "merged" | "separated" | "kept"; + reason: string; + target_id: number | null; + created_at: string; + undone_at?: string | null; + undone_by?: string | null; + undoable?: boolean; + source_id?: number | null; + target_revision?: number | null; +}; +export type GoalAutoReview = { + reviews: GoalAutoReviewRecord[]; + duplicate_judgments: GoalDuplicateJudgment[]; + closure: GoalAutoReviewRecord | null; + reopenable: boolean; +}; +export type GoalAutoAction = + | (GoalAutoReviewRecord & { + type: "review"; + goal: Pick; + current: boolean; + reopenable: boolean; + evidence_messages: GoalEvidenceMessage[]; + }) + | (GoalDuplicateJudgment & { + type: "duplicate"; + goals: Pick< + Goal, + "id" | "content" | "state" | "origin" | "merged_into" | "revision" + >[]; + }); export type GoalCatalog = { people: (Person & { kind?: string })[]; entries: Pick[]; @@ -244,6 +312,7 @@ export type GoalNotification = { }; export type GoalDetail = Goal & { revision_history?: GoalHistoryStatus; + auto_review?: GoalAutoReview; dedup_review?: { state: "pending" | "running" | "done" | "cancelled"; method: string; @@ -397,6 +466,11 @@ export type MaintenanceRun = { finished_at: string | null; summary: MaintenanceSummary; }; +export type GoalAutoSummary = { + enabled: boolean; + skip_reason?: string; + stopped?: string; +} & Record; export type MaintenanceItem = { phase: string; item_key: string; @@ -409,6 +483,14 @@ export type MaintenanceItem = { after?: number; transition?: string; source_memory_id?: number; + kind?: string; + evidence?: number[]; + revision_after?: number | null; + goal_ids?: number[]; + target_id?: number | null; + merged_id?: number | null; + judgment_id?: number; + target_revision_after?: number | null; }; created_at: string; }; @@ -416,6 +498,8 @@ export type MaintenanceReport = MaintenanceRun & ConsolidationProjection & { timezone: string; question_expiry?: { count: number; goal_ids: number[] }; + goal_auto_review?: GoalAutoSummary; + goal_duplicate_review?: GoalAutoSummary; items: MaintenanceItem[]; }; export type TrialCatalog = { diff --git a/src/iris/admin.py b/src/iris/admin.py index 1b23ed95..9ef0ca8c 100644 --- a/src/iris/admin.py +++ b/src/iris/admin.py @@ -493,6 +493,10 @@ class GoalBasisClear(GoalReason): expected_revision: int = Field(gt=0, strict=True) +class GoalReopen(GoalReason): + expected_revision: int = Field(gt=0, strict=True) + + class NotificationQuery(Page): status: Literal["pending", "taken", "cancelled"] | None = None goal_id: int | None = Field(default=None, gt=0, le=9223372036854775807) @@ -1104,6 +1108,10 @@ def create_goal(payload: NewGoal): app.state.scheduler.wake() return result + @router.get("/goal-auto-actions") + def goal_auto_actions(query: Annotated[Page, Query()]): + return admin_data.goal_auto_actions(app.state.store, **query.model_dump()) + @router.get("/goals/{goal_id}") def goal(goal_id: GoalId): return admin_data.goal(app.state.store, goal_id, current=app.state.goals.clock()) @@ -1126,6 +1134,18 @@ def edit_goal(goal_id: GoalId, payload: AdminGoalPatch): app.state.scheduler.wake() return result + @router.post("/goal-duplicate-judgments/{judgment_id}/undo") + def undo_goal_auto_merge(judgment_id: GoalId, payload: GoalReopen): + result = app.state.goals.undo_auto_merge(judgment_id, **payload.model_dump(), actor="admin") + app.state.scheduler.wake() + return result + + @router.post("/goals/{goal_id}/reopen") + def reopen_goal(goal_id: GoalId, payload: GoalReopen): + result = app.state.goals.reopen(goal_id, **payload.model_dump(), actor="admin") + app.state.scheduler.wake() + return result + @router.post("/goals/{goal_id}/duplicates/{other_id}/merge") def merge_goal(goal_id: GoalId, other_id: GoalId, payload: GoalDuplicate): result = app.state.goals.merge(goal_id, other_id, **payload.model_dump(), actor="admin") diff --git a/src/iris/admin_data.py b/src/iris/admin_data.py index 2d6913f8..ba8b8523 100644 --- a/src/iris/admin_data.py +++ b/src/iris/admin_data.py @@ -8,7 +8,7 @@ from .retrieval import Retrieval from .state import report_history -from .goals import goal_list, goal_detail, notification_list +from .goals import auto_actions, auto_closure, auto_review_projection, goal_list, goal_detail, notification_list from .people import list_people, person_detail, canonical_subject from .maintenance import abandoned_retry_days from .memory_ops import lifecycle_settings, missing_batch_targets @@ -557,7 +557,17 @@ def state_reports(store, *, limit=30, offset=0): def goals(store, **filters): with store.read() as conn: - return goal_list(conn, **filters) + result = goal_list(conn, **filters) + for item in result['items']: + # Admin-only marker; host projections of goals are unchanged. + closing = auto_closure(conn, item['id']) + item['auto_closed'] = closing is not None + return result + + +def goal_auto_actions(store, *, limit=30, offset=0): + with store.read() as conn: + return auto_actions(conn, limit=limit, offset=offset) def _goal_source_context(conn, message_id): @@ -596,6 +606,8 @@ def goal(store, goal_id, *, current=None): detail = goal_detail(conn, goal_id, current=current) for source in detail['sources']: source.update(_goal_source_context(conn,source['id'])) + detail['auto_review'] = auto_review_projection(conn, goal_id) + detail['auto_closed'] = detail['auto_review']['closure'] is not None return detail diff --git a/src/iris/goals.py b/src/iris/goals.py index 0d00d225..e38f3d90 100644 --- a/src/iris/goals.py +++ b/src/iris/goals.py @@ -7,6 +7,7 @@ from __future__ import annotations import difflib +import hashlib import json import re import threading @@ -14,21 +15,30 @@ import weakref from uuid import uuid4 from contextlib import nullcontext +from importlib.resources import files from datetime import datetime, time, timedelta, timezone from .runtime_journal import record as runtime_record from .claim_sequences import normalize_claim, same_claim_sequences from .db import dumps -from .tokens import Scope +from .tokens import Scope, TokenError from .memory_ops import Visibility, operation from .model_health import utc_now from .models import ModelError from . import goal_dedup_judge as dedup_judge from .people import canonical_subject from .query_analysis import SubjectNames +from .search_text import query_coverage_terms from .state import local_time, role_zone +# Full-auto G line switches (DECISIONS 2026-11-11): off for new and old +# databases until the planner enables them after the hidden-set gates. +AUTO_REVIEW_KEY = 'goal_auto_review_enabled' +DUPLICATE_AUTO_KEY = 'goal_duplicate_auto_judge_enabled' DEFAULTS = {'default_reminder_minutes': 60, 'overdue_reminders': True} +# Not written with other goal settings until set, so "never saved" stays +# distinguishable from an explicit choice when a default changes later. +AUTO_DEFAULTS = {AUTO_REVIEW_KEY: False, DUPLICATE_AUTO_KEY: False} QUESTION_EXPIRY_KEY = 'internal_question_expiry_days' DEFAULT_QUESTION_EXPIRY_DAYS = 7 ALL_ENTRIES = Scope(kind='all') @@ -57,7 +67,7 @@ def _stored_goal_settings(conn): def goal_settings(conn): - return _stored_goal_settings(conn) + return {**AUTO_DEFAULTS, **_stored_goal_settings(conn)} def _question_expiry_days(value): @@ -862,7 +872,11 @@ def _merge(conn,a,b,current,*,actor,semantic=False,reason=None,scope=ALL_ENTRIES # Preserve an explicit lead; with two different leads use the earlier warning. leads = [r['reminder_minutes'] for r in (target,source) if r['reminder_minutes'] is not None] lead = max(leads) if leads else None - conn.execute('UPDATE goals SET deadline=?,deadline_at=?,reminder_minutes=? WHERE id=?',(deadline,deadline_at,lead,tid)) + # Evidence an administrator rejected by reopening either goal stays rejected. + marks = [r[0] for r in conn.execute('SELECT auto_review_after_message_id FROM goals WHERE id IN (?,?)',(tid,sid)) + if r[0] is not None] + conn.execute('UPDATE goals SET deadline=?,deadline_at=?,reminder_minutes=?,auto_review_after_message_id=? WHERE id=?', + (deadline,deadline_at,lead,max(marks) if marks else None,tid)) for table, columns in (('goal_sources','message_id'),('goal_people','subject_id'),('goal_memories','memory_id,memory_revision')): conn.execute(f'INSERT OR IGNORE INTO {table}(goal_id,{columns}) SELECT ?,{columns} FROM {table} WHERE goal_id=?',(tid,sid)) # Copy warnings and clear receipts; an existing target receipt wins for the @@ -932,6 +946,81 @@ def _sequence_conflicts(conn,candidates,proposed,options,*,dismissed=()): return dedup_judge.select_sequence_conflicts(candidates,proposed,source_speakers=speakers,dismissed=dismissed) +def dedup_review_material(conn, goal_id, options, *, scope=ALL_ENTRIES): + """Candidate selection and model material for one goal's dedup judgment. + + The single assembly used by the creation-time review (Goals.review, which the + frozen goal_dedup_probe drives) and by daily maintenance (G2). Read-only; + callers snapshot inside a read transaction and call the model afterwards. + """ + proposed = _row(conn,goal_id) + clause, args = scope.sql('entry_id', write=True) + candidates = _rows(conn,"state='open' AND merged_into IS NULL AND kind=? AND id!=? AND "+clause,(proposed['kind'],goal_id,*args)) + visibility = Visibility(conn) + candidates = [c for c in candidates if visibility.goal(c['id']) == visibility.goal(goal_id)] + observed_scope = visibility.goal(goal_id) + zone = role_zone(conn) + for row in [proposed,*candidates]: + row['deadline'] = _deadline(row['deadline'],zone,strict=False) or row['deadline'] + row['deadline_at'] = row['deadline_at'] or _deadline(row['deadline'],zone,strict=False) + dismissed = {r[0] for r in conn.execute("""SELECT CASE WHEN goal_a=? THEN goal_b ELSE goal_a END + FROM goal_duplicates WHERE status='dismissed' AND (goal_a=? OR goal_b=?)""",(goal_id,goal_id,goal_id))} + selected = dedup_judge.select_candidates(candidates,proposed,dismissed=dismissed,limit=dedup_judge.MAX_CANDIDATES+1) + overflow = len(selected)>dedup_judge.MAX_CANDIDATES + selected = selected[:dedup_judge.MAX_CANDIDATES] + conflicts = _sequence_conflicts(conn,candidates,proposed,options,dismissed=dismissed) + observed = [proposed,*selected,*conflicts] + snapshots = [dedup_judge.material(conn,row) for row in observed] + model_possible = dedup_judge.model_possible(options) + return {'proposed':proposed,'selected':selected,'conflicts':conflicts,'overflow':overflow, + 'observed':observed,'snapshots':snapshots,'observed_scope':observed_scope, + 'model_possible':model_possible,'incoming':snapshots[0], + 'materials':snapshots[1:] if model_possible else snapshots[1:1+len(selected)], + 'source_snapshot':dedup_judge.source_speakers(conn,observed) if conflicts else {}, + 'revisions':{str(row['id']):row['revision'] for row in [*selected,*conflicts]}} + + +def dedup_review_judge(gateway, review, options, *, budget_seconds): + """The model call for one assembled review; outside any transaction.""" + if not review['materials']: + return {} + method = (dedup_judge.candidate_method(options) if review['model_possible'] + else dedup_judge.method(options)) + return dedup_judge.judge(gateway,review['incoming'],review['materials'],method=method,budget_seconds=budget_seconds) + + +def dedup_review_decision(review, verdicts, options, *, origin): + conflicts = review['conflicts'] + model_possible = review['model_possible'] + return dedup_judge.decision_from_verdicts(verdicts,origin=origin,overflow=review['overflow'], + possible_ids=[] if model_possible else [row['id'] for row in conflicts], + guard_ids=[row['id'] for row in conflicts] if model_possible else [], + scores={row['id']:dedup_judge.similarity(row,review['proposed']) for row in [*review['selected'],*conflicts]}, + min_similarity=options['possible_min_similarity']) + + +def dedup_review_stale(conn, review, *, scope=ALL_ENTRIES): + """True when any observed goal, scope, material or source speaker changed.""" + observed = review['observed'] + latest = {row['id']:_row(conn,row['id']) for row in observed} + stale = any(latest[row['id']]['revision']!=row['revision'] or latest[row['id']]['state']!='open' + or latest[row['id']]['merged_into'] is not None + or not scope.allows_write(latest[row['id']]['entry_id']) for row in observed) + visibility = Visibility(conn) + stale = stale or any(visibility.goal(row['id']) != review['observed_scope'] for row in observed) + # Subject aliases and source excerpts are not covered by goal revisions. + if not stale: + for row,snapshot in zip(observed,review['snapshots']): + check = latest[row['id']] + check['deadline'] = _deadline(check['deadline'],role_zone(conn),strict=False) or check['deadline'] + if dedup_judge.material(conn,check)!=snapshot: + stale = True + break + if review['conflicts'] and not stale: + stale = dedup_judge.source_speakers(conn,observed)!=review['source_snapshot'] + return stale + + def _create(conn, *, content, kind='normal', deadline=None, reminder_minutes=None, people=(), entry_id=None, host_key=None, host=None, origin='host', actor='host', current=None, evidence=(), learning=False, scope=ALL_ENTRIES): current = _current(current) @@ -1041,6 +1130,772 @@ def write_learning_goal(conn, *, content, kind, deadline, entry_id, evidence, cu entry_id=entry_id,people=people,evidence=evidence,origin='internal',actor='learning',current=current,learning=True) + + +# --------------------------------------------------------------------------- +# G line (full-auto stage): daily maintenance judgments of goals. +# G1 closes learning-created normal goals only on explicit message evidence; +# G2 resolves "possible duplicate" pairs with the shared dedup assembly above. +# The model only judges and cites; code checks and writes. Every model call is +# outside a transaction and every write rechecks the observed goal revision. +# --------------------------------------------------------------------------- + +AUTO_REVIEW_PROMPT_VERSION = 'goal_review_auto_v1' +AUTO_REVIEW_PURPOSE = 'goal_auto_review' # model kind 'chat'; no new purpose +AUTO_REVIEW_MAX_MESSAGES = 30 +AUTO_REVIEW_LATEST_MESSAGES = 10 # newest qualified messages always kept, so a later retraction stays visible +AUTO_REVIEW_MAX_SOURCES = 8 +AUTO_REVIEW_TEXT_LIMIT = 1000 +AUTO_REVIEW_MAX_TOKENS = 16000 +AUTO_REVIEW_MAX_KEYWORDS = 16 +AUTO_REVIEW_MAX_CALLS = 100 # model judgments per maintenance pass; the rest waits +DUPLICATE_AUTO_BUDGET_SECONDS = 60.0 # per pair, not the 5 s inline budget +DUPLICATE_AUTO_MAX_CALLS = 50 +EVIDENCE_KINDS = ('message','self_output','action_result') +# Fixed kept-open reasons; each is recorded with its fingerprint. +AUTO_REVIEW_KEPT_REASONS = ('decision_open','invalid_output','content_rejection','evidence_empty', + 'evidence_outside_material','evidence_only_sources','evidence_missing','evidence_not_after_creation', + 'evidence_sender_not_involved','evidence_kind_not_allowed') +DUPLICATE_AUTO_REASONS = ('auto_merged','auto_different','admin_not_merged','host_scope_not_merged','uncertain', + 'same_ambiguous','partner_not_in_material','partner_not_judged','merge_blocked') +_AUTO_REVIEW_GOALS = """g.origin='internal' AND g.kind='normal' AND g.state='open' AND g.merged_into IS NULL + AND NOT EXISTS(SELECT 1 FROM goals absorbed WHERE absorbed.merged_into=g.id AND absorbed.origin!='internal')""" + + +def auto_review_prompt(): + return files('iris').joinpath(f'prompts/{AUTO_REVIEW_PROMPT_VERSION}.md').read_text(encoding='utf-8') + + +def auto_review_switches(conn): + settings = goal_settings(conn) + return {'review':settings[AUTO_REVIEW_KEY] is True,'duplicates':settings[DUPLICATE_AUTO_KEY] is True} + + +def _instant(value): + stamp = datetime.fromisoformat(value) + if stamp.tzinfo is None: + raise ValueError('timestamp requires a timezone') + return stamp.astimezone(timezone.utc) + + +def auto_review_goal_ids(conn, *, goal_through=None, run_id=None): + """Learning-created, normal, open, unmerged goals that absorbed no host/admin goal. + + Least recently reviewed first: never reviewed, then by the last judgment + before this run. Returns (id, revision, order key); a goal left over by the + per-run call cap is therefore first in line the next time. + """ + rows = conn.execute("""SELECT g.id,g.revision,COALESCE((SELECT MAX(r.created_at) FROM goal_auto_reviews r + WHERE r.goal_id=g.id AND (? IS NULL OR r.run_id IS NULL OR r.run_id!=?)),'') AS reviewed + FROM goals g WHERE """+_AUTO_REVIEW_GOALS+(' AND g.id<=?' if goal_through is not None else '')+ + ' ORDER BY reviewed,g.id',(run_id,run_id,*((goal_through,) if goal_through is not None else ()))) + return [(r[0],r[1],r[2]) for r in rows] + + +def _auto_review_people(conn, goal): + """Goal people, or the senders of its source messages when it names nobody.""" + if goal['people']: + return list(goal['people']) + senders = set() + for row in conn.execute('''SELECT DISTINCT m.sender_subject_id FROM goal_sources gs + JOIN messages m ON m.id=gs.message_id WHERE gs.goal_id=?''',(goal['id'],)): + senders.add(canonical_subject(conn,row[0])) + return sorted(senders-{'self','scene'}) + + +def _evidence_entries(conn, goal_id): + """Entries visible wherever the goal is visible; None means every entry. + + A closure may not carry information out of an entry that the goal's own + audience cannot see. + """ + visibility = Visibility(conn) + goal_scope = visibility.goal(goal_id) + allowed = sorted(eid for eid,scope in visibility.entries.items() + if scope is None or (goal_scope is not None and goal_scope <= scope)) + return None if len(allowed)==len(visibility.entries) else allowed + + +def _goal_keywords(text): + return [term for term in query_coverage_terms(text) if len(term)>=2][:AUTO_REVIEW_MAX_KEYWORDS] + + +def _subject_info(conn, sid): + row = conn.execute('SELECT name FROM subjects WHERE id=?',(sid,)).fetchone() + return {'name':row[0] if row else sid, + 'aliases':[r[0] for r in conn.execute('SELECT alias FROM subject_aliases WHERE subject_id=? ORDER BY alias',(sid,))]} + + +_MESSAGE_COLUMNS = 'm.id,m.entry_id,m.kind,m.sender_subject_id,m.content,m.quote_author_subject_id,m.quote_content,m.occurred_at' + + +def auto_review_material(conn, goal_id, *, current, message_through=None): + """G1 model material for one goal, or None (ineligible, or no candidate message). + + Candidates are messages after the goal's creation (and after an administrator + reopen) from its people or self, never the goal's own sources. The newest 10 + are always kept; the rest come from the goal entry and goal keywords first, + then the most recent, at most 30 in all. The fingerprint uses the newest + qualified message ID, so any new qualified message is judged again. Only goal + facts and messages enter the material. + """ + if conn.execute('SELECT 1 FROM goals g WHERE g.id=? AND '+_AUTO_REVIEW_GOALS,(goal_id,)).fetchone() is None: + return None + goal = _row(conn,goal_id) + zone = role_zone(conn) + try: + created = _instant(goal['created_at']) + except (TypeError,ValueError): + return None # A legacy creation time without a zone cannot anchor rule 2. + people = _auto_review_people(conn,goal) + if message_through is None: + message_through = conn.execute('SELECT COALESCE(MAX(id),0) FROM messages').fetchone()[0] + keywords = _goal_keywords(goal['content']) + entries = _evidence_entries(conn,goal_id) + params = {'people':dumps(sorted(people)),'entry':goal['entry_id'],'gid':goal_id, + 'after':goal['auto_review_after_message_id'] or 0,'through':message_through, + 'created':goal['created_at'],'entries':dumps(entries or []), + **{f'k{i}':keyword for i,keyword in enumerate(keywords)}} + keyword_sql = ' OR '.join(f'instr(lower(m.content),lower(:k{i}))>0' for i in range(len(keywords))) or '0' + entry_sql = ' AND m.entry_id IN (SELECT value FROM json_each(:entries))' if entries is not None else '' + # julianday narrows the scan; the exact instant comparison follows in Python. + # The people's own messages come from any visible entry. The role's own + # messages count only in this goal's conversation: its entry and the entries + # where its people have spoken since the goal was created, so chatter + # elsewhere neither enters the material nor changes the fingerprint. + pool = f"""WITH RECURSIVE tree(id) AS ( + SELECT value FROM json_each(:people) + UNION SELECT s.id FROM subjects s JOIN tree t ON s.merged_into=t.id + ), conversation(entry_id) AS ( + SELECT :entry WHERE :entry IS NOT NULL + UNION SELECT DISTINCT x.entry_id FROM messages x + WHERE x.id<=:through AND x.sender_subject_id IN (SELECT id FROM tree) + AND julianday(x.occurred_at)>julianday(:created) + ) SELECT {_MESSAGE_COLUMNS},(m.entry_id IS :entry) AS same_entry,({keyword_sql}) AS keyword + FROM messages m WHERE m.id>:after AND m.id<=:through + AND (m.sender_subject_id IN (SELECT id FROM tree) + OR (m.sender_subject_id='self' AND m.entry_id IN (SELECT entry_id FROM conversation))) + AND julianday(m.occurred_at)>=julianday(:created)-1.0/86400 + AND m.id NOT IN (SELECT message_id FROM goal_sources WHERE goal_id=:gid){entry_sql}""" + + def qualified(order, limit): + result = [] + for row in conn.execute(pool+' ORDER BY '+order+f' LIMIT {int(limit)}',params).fetchall(): + try: + if _instant(row['occurred_at'])>created: + result.append(dict(row)) + except ValueError: + continue + return result + + latest = qualified('julianday(m.occurred_at) DESC,m.id DESC',4*AUTO_REVIEW_LATEST_MESSAGES) + if not latest: + return None + picked = latest[:AUTO_REVIEW_LATEST_MESSAGES] + chosen = {row['id'] for row in picked} + for row in qualified('(same_entry AND keyword) DESC,(same_entry OR keyword) DESC,julianday(m.occurred_at) DESC,m.id DESC',200): + if len(picked)>=AUTO_REVIEW_MAX_MESSAGES: + break + if row['id'] not in chosen: + picked.append(row) + chosen.add(row['id']) + # Highest qualified message ID, inside or outside the material (fingerprint). + newest = conn.execute('SELECT MAX(id) FROM ('+pool+')',params).fetchone()[0] or 0 + picked.sort(key=lambda r:(_instant(r['occurred_at']),r['id'])) + sources = [dict(r) for r in conn.execute(f'''SELECT {_MESSAGE_COLUMNS} FROM goal_sources gs + JOIN messages m ON m.id=gs.message_id WHERE gs.goal_id=? ORDER BY m.id DESC LIMIT ?''', + (goal_id,AUTO_REVIEW_MAX_SOURCES+1))] + sources_truncated = len(sources)>AUTO_REVIEW_MAX_SOURCES + sources = list(reversed(sources[:AUTO_REVIEW_MAX_SOURCES])) + entry_labels, person_labels = {}, {} + + def entry_label(eid): + if eid is None: + return None + return entry_labels.setdefault(eid,f'E{len(entry_labels)+1}') + + def person_label(sid): + if sid is None: + return None + sid = canonical_subject(conn,sid) + if sid in ('self','scene'): + return '我' if sid=='self' else '场景' + return person_labels.setdefault(sid,f'P{len(person_labels)+1}') + + def message(row): + item = {'id':row['id'],'entry':entry_label(row['entry_id']),'sender':person_label(row['sender_subject_id']), + 'kind':row['kind'],'at':local_time(row['occurred_at'],zone),'text':row['content'][:AUTO_REVIEW_TEXT_LIMIT]} + if len(row['content'])>AUTO_REVIEW_TEXT_LIMIT: + item['truncated'] = True + if row['quote_author_subject_id'] or row['quote_content']: + item['quote'] = {'author':person_label(row['quote_author_subject_id']), + 'text':(row['quote_content'] or '')[:AUTO_REVIEW_TEXT_LIMIT]} + return item + + entry_label(goal['entry_id']) + goal_people = [person_label(sid) for sid in people] + source_items = [message(row) for row in sources] + message_items = [message(row) for row in picked] + deadline = goal['deadline_at'] or _deadline(goal['deadline'],zone,strict=False) + entry_rows = {r['id']:r for r in conn.execute('SELECT id,name,kind FROM entries WHERE id IN (SELECT value FROM json_each(?))', + (dumps(list(entry_labels)),))} + payload = {'now':local_time(_current(current).isoformat(),zone), + 'goal':{'text':goal['content'],'created_at':local_time(goal['created_at'],zone), + 'deadline':local_time(deadline,zone) if deadline else goal['deadline'], + 'entry':entry_label(goal['entry_id']),'people':goal_people}, + 'people':[{'id':label,**_subject_info(conn,sid)} for sid,label in person_labels.items()], + 'entries':[{'id':label,'name':entry_rows[eid]['name'] if eid in entry_rows else None, + 'kind':entry_rows[eid]['kind'] if eid in entry_rows else None} + for eid,label in entry_labels.items()], + 'sources':source_items,'messages':message_items} + if sources_truncated: + payload['sources_truncated'] = True + ids = [row['id'] for row in [*sources,*picked]] + return {'goal_id':goal_id,'revision':goal['revision'],'latest_message_id':max(newest,*ids), + 'message_ids':ids,'candidate_ids':[row['id'] for row in picked],'payload':payload} + + +def auto_review_judged(conn, material): + return conn.execute('''SELECT 1 FROM goal_auto_reviews WHERE goal_id=? AND goal_revision=? + AND latest_message_id=?''',(material['goal_id'],material['revision'],material['latest_message_id'])).fetchone() is not None + + +def validate_goal_review(value): + """Strict shape of the model answer; anything else is a kept-open quality result.""" + invalid = {'decision':'invalid','evidence':[],'invalid_reason':'invalid_output'} + if (not isinstance(value,dict) or not {'decision','evidence'}<=set(value) + or set(value)-{'decision','evidence','reason'}): + return invalid + decision, evidence = value['decision'], value['evidence'] + if (decision not in ('completed','abandoned','open') or not isinstance(evidence,list) + or any(type(item) is not int for item in evidence) + or ('reason' in value and not isinstance(value['reason'],str))): + return invalid + return {'decision':decision,'evidence':list(dict.fromkeys(evidence))} + + +def auto_review_admission(gateway): + """Preflight like the other maintenance chat steps: pause and the daily token limit.""" + if gateway is None or not hasattr(gateway,'json_chat'): + raise ModelError('configuration','goal review unavailable',reason='unconfigured') + health = getattr(gateway,'health',None) + if health is not None: + health.check('chat','learning') + + +def duplicate_auto_admission(gateway): + if gateway is None or not hasattr(gateway,'goal_dedup_judge'): + raise ModelError('configuration','goal judgment unavailable',reason='unconfigured') + health = getattr(gateway,'health',None) + if health is not None: + health.check('goal_dedup_judge','goal_dedup_judge') + + +def judge_goal_review(gateway, material): + """One G1 judgment through the chat purpose. Never call inside a transaction.""" + auto_review_admission(gateway) + messages = [{'role':'system','content':auto_review_prompt()}, + {'role':'user','content':json.dumps(material['payload'],ensure_ascii=False)}] + try: + parsed = gateway.json_chat(messages,AUTO_REVIEW_PURPOSE,AUTO_REVIEW_MAX_TOKENS)[0] + except ModelError as exc: + if exc.raw_output is not None: # still unparseable after the one JSON repair + return {'decision':'invalid','evidence':[],'invalid_reason':'invalid_output'} + if exc.category=='content_rejection': + return {'decision':'invalid','evidence':[],'invalid_reason':'content_rejection'} + raise + return validate_goal_review(parsed) + + +def _review_check(conn, goal, material, result): + """Section 3 checks against the current rows; a fixed kept reason, or None to close.""" + if result['decision']=='invalid': + return result['invalid_reason'] + if result['decision']=='open': + return 'decision_open' + evidence = result['evidence'] + if not evidence: + return 'evidence_empty' + if not set(evidence)<=set(material['message_ids']): + return 'evidence_outside_material' + # The goal's own sources (including those inherited from a merged goal) are + # where it came from, never proof that it is done, whatever their time. + sources = {r[0] for r in conn.execute('SELECT message_id FROM goal_sources WHERE goal_id=?',(goal['id'],))} + cited = [mid for mid in evidence if mid in set(material['candidate_ids'])-sources] + if not cited: + return 'evidence_only_sources' + rows = conn.execute('''SELECT id,kind,sender_subject_id,occurred_at FROM messages + WHERE id IN (SELECT value FROM json_each(?))''',(dumps(cited),)).fetchall() + if len(rows)!=len(cited): + return 'evidence_missing' + created = _instant(goal['created_at']) + after = goal['auto_review_after_message_id'] or 0 + later = [r for r in rows if r['id']>after and _instant(r['occurred_at'])>created] + if not later: + return 'evidence_not_after_creation' + allowed = {*_auto_review_people(conn,goal),'self'} + involved = [r for r in later if canonical_subject(conn,r['sender_subject_id']) in allowed] + if not involved: + return 'evidence_sender_not_involved' + if not any(r['kind'] in EVIDENCE_KINDS for r in involved): + return 'evidence_kind_not_allowed' + return None + + +def _qualifying_evidence(conn, goal, material, evidence): + """Cited messages that each pass every rule-2 check on their own.""" + sources = {r[0] for r in conn.execute('SELECT message_id FROM goal_sources WHERE goal_id=?',(goal['id'],))} + cited = [mid for mid in evidence if mid in set(material['candidate_ids'])-sources] + created = _instant(goal['created_at']) + after = goal['auto_review_after_message_id'] or 0 + allowed = {*_auto_review_people(conn,goal),'self'} + rows = conn.execute('''SELECT id,kind,sender_subject_id,occurred_at FROM messages + WHERE id IN (SELECT value FROM json_each(?))''',(dumps(cited),)).fetchall() + return sorted(r['id'] for r in rows if r['id']>after and _instant(r['occurred_at'])>created + and r['kind'] in EVIDENCE_KINDS and canonical_subject(conn,r['sender_subject_id']) in allowed) + + +def apply_goal_review(conn, current, *, material, result, run_id=None): + """Record one G1 judgment and, when every check passes, close the goal. + + Runs in the caller's write transaction. GoalConflict means the goal changed + after the material was read; nothing is recorded and a later run retries. + Returns None when this fingerprint was already recorded. + """ + if not conn.in_transaction: + raise ValueError('goal auto review requires a write transaction') + current = _current(current) + stamp = current.isoformat() + gid = material['goal_id'] + goal = _row(conn,gid) + if (goal['revision']!=material['revision'] or + conn.execute('SELECT 1 FROM goals g WHERE g.id=? AND '+_AUTO_REVIEW_GOALS,(gid,)).fetchone() is None): + raise GoalConflict() + if auto_review_judged(conn,material): + return None + kept = _review_check(conn,goal,material,result) + state = result['decision'] if kept is None else None + revision_after = snapshot_id = None + if state is not None: + before = _snapshot(conn,gid) + changed = conn.execute("""UPDATE goals SET state=?,closed_at=?,closed_by='maintenance',revision=revision+1, + updated_at=? WHERE id=? AND revision=? AND state='open' AND merged_into IS NULL AND origin='internal'""", + (state,stamp,stamp,gid,goal['revision'])).rowcount + if changed!=1: + raise GoalConflict() + # Same reminder handling as an explicit state change. + _cancel_review(conn,gid,current,{'status':'created','target_id':None,'reason':'goal_closed'}) + _cancel(conn,gid,current,journal_reason='goal_closed') + _schedule(conn,_row(conn,gid),current) + snapshot_id = _record_snapshot(conn,gid,before,actor='maintenance',action='auto_review', + reason='auto_'+state,current=current) + revision_after = _row(conn,gid)['revision'] + reason = 'auto_'+state if state is not None else kept + review_id = conn.execute('''INSERT INTO goal_auto_reviews(goal_id,goal_revision,latest_message_id,run_id, + decision,outcome,reason,evidence_json,material_ids_json,prompt_version,revision_after,created_at) + VALUES(?,?,?,?,?,?,?,?,?,?,?,?)''',(gid,goal['revision'],material['latest_message_id'],run_id, + result['decision'],'closed' if state is not None else 'kept_open',reason,dumps(result['evidence']), + dumps(material['message_ids']),AUTO_REVIEW_PROMPT_VERSION,revision_after,stamp)).lastrowid + if state is not None: + # Protect the qualifying evidence from message retention (memory_ops.message_references). + conn.executemany('INSERT OR IGNORE INTO goal_auto_review_evidence(review_id,message_id) VALUES(?,?)', + [(review_id,mid) for mid in _qualifying_evidence(conn,goal,material,result['evidence'])]) + operation(conn,'goal_auto_review','goal',gid,{'decision':state,'evidence':result['evidence'], + 'review_id':review_id,'revision_before':goal['revision'],'snapshot_id':snapshot_id,'run_id':run_id}, + actor='maintenance',stamp=stamp) + return {'review_id':review_id,'goal_id':gid,'outcome':'closed' if state is not None else 'kept_open', + 'decision':result['decision'],'reason':reason,'evidence':result['evidence'], + 'revision_before':goal['revision'],'revision_after':revision_after, + 'latest_message_id':material['latest_message_id'],'candidates':len(material['candidate_ids'])} + + +def duplicate_auto_pairs(conn, *, goal_through=None): + return [(r[0],r[1]) for r in conn.execute("""SELECT d.goal_a,d.goal_b FROM goal_duplicates d + JOIN goals a ON a.id=d.goal_a JOIN goals b ON b.id=d.goal_b + WHERE d.status='possible' AND a.state='open' AND b.state='open' + AND a.merged_into IS NULL AND b.merged_into IS NULL"""+(' AND d.goal_b<=?' if goal_through is not None else '')+ + ' ORDER BY d.goal_a,d.goal_b',(goal_through,) if goal_through is not None else ())] + + +def _pair_status(conn, goal_a, goal_b): + row = conn.execute('SELECT status FROM goal_duplicates WHERE goal_a=? AND goal_b=?',(goal_a,goal_b)).fetchone() + return row[0] if row else None + + +def duplicate_auto_material(conn, goal_a, goal_b): + """The shared dedup assembly for the later goal of one possible pair. + + Returns {'skip': reason} when the pair cannot be judged now. The fingerprint + covers every observed goal revision and the newest source message. + """ + goal_a,goal_b = sorted((goal_a,goal_b)) + options = dedup_judge.connection_settings(conn) + if not dedup_judge.model_possible(options): + return {'skip':'dedup_judge_disabled'} + rows = {gid:_row(conn,gid) for gid in (goal_a,goal_b)} + if (_pair_status(conn,goal_a,goal_b)!='possible' + or any(r['state']!='open' or r['merged_into'] is not None for r in rows.values())): + return {'skip':'pair_changed'} + if conn.execute("""SELECT 1 FROM goal_dedup_jobs WHERE goal_id IN (?,?) + AND state IN ('pending','running')""",(goal_a,goal_b)).fetchone(): + return {'skip':'review_pending'} + partner,proposed = sorted(rows.values(),key=lambda r:(r['created_at'],r['id'])) + method = dedup_judge.candidate_method(options) + # The later goal is judged (and would be merged) under its own token scope, + # exactly as in the creation-time review. Only a scoped host goal that would + # be merged into a goal outside its scope is blocked; the hint stays. + scopes = {gid:Scope.model_validate_json(rows[gid]['host_scope_json'] or '{"kind":"all"}') for gid in rows} + common = {'goal_a':goal_a,'goal_b':goal_b,'proposed_id':proposed['id'],'partner_id':partner['id'], + 'origins':sorted({r['origin'] for r in rows.values()}),'options':options,'method':method, + 'revisions':{str(gid):rows[gid]['revision'] for gid in rows}} + # Merging the later goal also redirects every goal absorbed into it, so each + # absorbed host goal's token scope must allow the earlier goal's entry too. + absorbed = [r[0] for r in conn.execute('''WITH RECURSIVE chain(id) AS ( + SELECT ? UNION SELECT g.id FROM goals g JOIN chain c ON g.merged_into=c.id) + SELECT g.host_scope_json FROM goals g JOIN chain c ON c.id=g.id WHERE g.id!=?''', + (proposed['id'],proposed['id']))] + moved = [scopes[proposed['id']],*(Scope.model_validate_json(value or '{"kind":"all"}') for value in absorbed)] + if any(not scope.allows_write(partner['entry_id']) for scope in moved): + fingerprint = hashlib.sha256(json.dumps({'scope_blocked':True,'goals':sorted(common['revisions'].items())}, + sort_keys=True).encode()).hexdigest() + return {**common,'scope_blocked':True,'fingerprint':fingerprint,'partner_in_material':False} + scope = scopes[proposed['id']] + review = dedup_review_material(conn,proposed['id'],options,scope=scope) + judged = [row['id'] for row in review['selected']] + if review['model_possible']: + judged += [row['id'] for row in review['conflicts']] + observed = sorted({row['id']:row['revision'] for row in review['observed']}.items()) + latest = conn.execute('''SELECT COALESCE(MAX(message_id),0) FROM goal_sources + WHERE goal_id IN (SELECT value FROM json_each(?))''',(dumps([gid for gid,_ in observed]),)).fetchone()[0] + fingerprint = hashlib.sha256(json.dumps({'method':method,'goals':observed,'latest_message_id':latest}, + sort_keys=True).encode()).hexdigest() + return {**common,'review':review,'fingerprint':fingerprint,'partner_in_material':partner['id'] in judged, + 'scope':scope,'merge_scope':scope} + + +def duplicate_auto_judged(conn, material): + return conn.execute('SELECT 1 FROM goal_duplicate_judgments WHERE goal_a=? AND goal_b=? AND fingerprint=?', + (material['goal_a'],material['goal_b'],material['fingerprint'])).fetchone() is not None + + +def judge_duplicate_pair(gateway, material): + """The D6 judgment of the shared material at the 60 s maintenance budget.""" + return dedup_review_judge(gateway,material['review'],material['options'],budget_seconds=DUPLICATE_AUTO_BUDGET_SECONDS) + + +def _dismiss_pair(conn, goal_id, other_id, current, *, actor, reason): + a,b = sorted((goal_id,other_id)) + before = {gid:_snapshot(conn,gid) for gid in (a,b)} + conn.execute("UPDATE goal_duplicates SET status='dismissed',resolved_at=?,resolved_by=? WHERE goal_a=? AND goal_b=?", + (current.isoformat(),actor,a,b)) + _bump(conn,a,current) + _bump(conn,b,current) + return [_record_snapshot(conn,gid,before[gid],actor=actor,action='duplicate_dismiss',reason=reason,current=current) + for gid in (a,b)] + + +def apply_duplicate_judgment(conn, current, *, material, verdicts, run_id=None): + """Execute one G2 judgment in the caller's write transaction. + + same -> merge into the earlier goal (an administrator goal is never merged: + the hint is withdrawn instead); different -> withdraw the hint; uncertain or + any ambiguity -> keep it. GoalConflict means something observed changed. + """ + if not conn.in_transaction: + raise ValueError('goal duplicate judgment requires a write transaction') + current = _current(current) + stamp = current.isoformat() + a,b = material['goal_a'],material['goal_b'] + if material.get('scope_blocked'): + return _apply_scope_block(conn,current,material=material,run_id=run_id) + review = material['review'] + if _pair_status(conn,a,b)!='possible' or dedup_review_stale(conn,review,scope=material['scope']): + raise GoalConflict() + active = dedup_judge.connection_settings(conn) + if (not dedup_judge.model_possible(active) or dedup_judge.candidate_method(active)!=material['method'] + or active['possible_min_similarity']!=material['options']['possible_min_similarity']): + raise GoalConflict() + if duplicate_auto_judged(conn,material): + return None + partner,proposed = material['partner_id'],material['proposed_id'] + verdict = verdicts.get(partner) if material['partner_in_material'] else None + outcome,target = 'kept',None + if not material['partner_in_material']: + reason = 'partner_not_in_material' + elif verdict=='different': + outcome,reason = 'separated','auto_different' + elif verdict=='same': + # Merge only where the creation-time rule would merge into this partner: + # one same, no other ambiguity, no overflow and no sequence conflicts. + clean = dedup_review_decision(review,verdicts,material['options'],origin='internal') + if clean['status']=='merged' and clean['target_id']==partner: + outcome,reason = (('separated','admin_not_merged') if 'admin' in material['origins'] + else ('merged','auto_merged')) + else: + reason = 'same_ambiguous' + elif verdict=='uncertain': + reason = 'uncertain' + else: + reason = 'partner_not_judged' + record = None + if outcome=='merged': + rows = {row['id']:row for row in review['observed']} + try: + # _merge validates before writing, so a refusal leaves nothing behind. + record = _merge_record(conn,partner,proposed) + target = _merge(conn,rows[partner],rows[proposed],current,actor='maintenance',semantic=True,reason=reason, + scope=material['merge_scope']) + record.update(target_revision_after=_row(conn,partner)['revision'], + source_revision_after=_row(conn,proposed)['revision'],merged_at=stamp) + except (GoalError,TokenError): + outcome,reason,record = 'kept','merge_blocked',None + if outcome=='separated': + _dismiss_pair(conn,a,b,current,actor='maintenance',reason=reason) + judgment_id = conn.execute('''INSERT INTO goal_duplicate_judgments(goal_a,goal_b,fingerprint,run_id,method,verdict, + outcome,reason,target_id,merge_record_json,created_at) VALUES(?,?,?,?,?,?,?,?,?,?,?)''',(a,b,material['fingerprint'], + run_id,material['method'],verdict,outcome,reason,target,dumps(record) if record else None,stamp)).lastrowid + if outcome!='kept': + operation(conn,'goal_duplicate_auto_judged','goal',a,{'other_id':b,'verdict':verdict,'outcome':outcome, + 'reason':reason,'target_id':target,'judgment_id':judgment_id,'run_id':run_id}, + actor='maintenance',stamp=stamp) + return {'judgment_id':judgment_id,'goal_ids':[a,b],'outcome':outcome,'reason':reason,'verdict':verdict, + 'target_id':target,'merged_id':(proposed if target is not None else None),'method':material['method'], + 'target_revision_after':record['target_revision_after'] if record else None} + + +def _apply_scope_block(conn, current, *, material, run_id=None): + stamp = current.isoformat() + a,b = material['goal_a'],material['goal_b'] + rows = {gid:_row(conn,gid) for gid in (a,b)} + if (_pair_status(conn,a,b)!='possible' + or any(rows[gid]['revision']!=material['revisions'][str(gid)] or rows[gid]['state']!='open' + or rows[gid]['merged_into'] is not None for gid in rows)): + raise GoalConflict() + if duplicate_auto_judged(conn,material): + return None + # No model call and no goal write: the hint stays for the administrator. + reason = 'host_scope_not_merged' + judgment_id = conn.execute('''INSERT INTO goal_duplicate_judgments(goal_a,goal_b,fingerprint,run_id,method,verdict, + outcome,reason,target_id,created_at) VALUES(?,?,?,?,?,NULL,'kept',?,NULL,?)''', + (a,b,material['fingerprint'],run_id,material['method'],reason,stamp)).lastrowid + return {'judgment_id':judgment_id,'goal_ids':[a,b],'outcome':'kept','reason':reason,'verdict':None, + 'target_id':None,'merged_id':None,'method':material['method'],'target_revision_after':None} + + +def _merge_record(conn, target_id, source_id): + """IDs and field values that an automatic merge changes; no prose.""" + target = _row(conn,target_id) + ids = lambda sql,gid: [r[0] for r in conn.execute(sql,(gid,))] # noqa: E731 + return {'target_id':target_id,'source_id':source_id, + 'target_fields':{key:target[key] for key in ('deadline','deadline_at','reminder_minutes','auto_review_after_message_id')}, + 'target_sources':ids('SELECT message_id FROM goal_sources WHERE goal_id=?',target_id), + 'target_people':ids('SELECT subject_id FROM goal_people WHERE goal_id=?',target_id), + 'target_memories':ids('SELECT memory_id FROM goal_memories WHERE goal_id=?',target_id), + 'target_annotations':ids('SELECT id FROM goal_basis_annotations WHERE goal_id=?',target_id), + 'redirects':ids('SELECT id FROM goals WHERE merged_into=?',source_id), + 'pairs':[list(r) for r in conn.execute('''SELECT goal_a,goal_b,status FROM goal_duplicates + WHERE goal_a IN (?,?) OR goal_b IN (?,?)''',(target_id,source_id,target_id,source_id))], + 'notifications':[list(r) for r in conn.execute('''SELECT id,goal_id FROM notifications + WHERE goal_id IN (?,?) AND status='pending' ORDER BY id''',(target_id,source_id))]} + + +def _merge_undo_state(conn, row): + """(undoable, record) for one judgment row; undo needs the exact post-merge revisions.""" + if row['outcome']!='merged' or not row['merge_record_json'] or row['undone_at'] is not None: + return False,None + record = json.loads(row['merge_record_json']) + target = conn.execute('SELECT revision,merged_into FROM goals WHERE id=?',(record['target_id'],)).fetchone() + source = conn.execute('SELECT revision,merged_into FROM goals WHERE id=?',(record['source_id'],)).fetchone() + return (bool(target and source and target['revision']==record['target_revision_after'] + and target['merged_into'] is None and source['revision']==record['source_revision_after'] + and source['merged_into']==record['target_id']),record) + + +def _reschedule_current_stage(conn, goal_id, current): + """Rebuild one goal's plans; publish one reminder for the stage it is in now. + + Warning time passed but not yet due: a "due soon" reminder. Past the + deadline: an overdue reminder with the usual next-day plan, or, when overdue + reminders are turned off, one "due" reminder and no next-day plan. Before + the warning time nothing is published; the new plan sends it. + """ + row = _row(conn,goal_id) + _cancel(conn,goal_id,current,notifications=False) + publish = due_notice = False + if row['kind']=='normal' and row['state']=='open' and row['merged_into'] is None and row['deadline_at']: + deadline = datetime.fromisoformat(row['deadline_at']) + lead = row['reminder_minutes'] if row['reminder_minutes'] is not None else goal_settings(conn)['default_reminder_minutes'] + if _soon(deadline,lead)<=current: + if deadlinededup_judge.MAX_CANDIDATES - selected = selected[:dedup_judge.MAX_CANDIDATES] - conflicts = _sequence_conflicts(conn,candidates,proposed,options,dismissed=dismissed) - observed = [proposed,*selected,*conflicts] - snapshots = [dedup_judge.material(conn,row) for row in observed] - model_possible = dedup_judge.model_possible(options) - incoming = snapshots[0] - materials = snapshots[1:] if model_possible else snapshots[1:1+len(selected)] - source_snapshot = dedup_judge.source_speakers(conn,observed) if conflicts else {} - revisions = {str(row['id']):row['revision'] for row in [*selected,*conflicts]} + review = dedup_review_material(conn,goal_id,options,scope=scope) + proposed,selected,conflicts,observed = (review['proposed'],review['selected'], + review['conflicts'],review['observed']) + revisions = review['revisions'] with self.store.write() as conn: retained = conn.execute("""UPDATE goal_dedup_jobs SET input_revision=?,candidate_revisions_json=? WHERE goal_id=? AND state='running' AND lease_token=?""", @@ -1179,14 +2016,8 @@ def review(self,goal_id,*,actor='scheduler'): remaining = options['budget_seconds']-(timer.monotonic()-started) if remaining<=0: raise ModelError('retryable','goal judgment total timeout',reason='timeout') - verdicts = dedup_judge.judge(self.gateway,incoming,materials, - method=dedup_judge.candidate_method(options) if model_possible else dedup_judge.method(options), - budget_seconds=remaining) if materials else {} - decision = dedup_judge.decision_from_verdicts(verdicts,origin=proposed['origin'],overflow=overflow, - possible_ids=[] if model_possible else [row['id'] for row in conflicts], - guard_ids=[row['id'] for row in conflicts] if model_possible else [], - scores={row['id']:dedup_judge.similarity(row,proposed) for row in [*selected,*conflicts]}, - min_similarity=options['possible_min_similarity']) + verdicts = dedup_review_judge(self.gateway,review,options,budget_seconds=remaining) + decision = dedup_review_decision(review,verdicts,options,origin=proposed['origin']) except ModelError as exc: decision = {'status':'pending','target_id':None,'reason':exc.reason or exc.category} with self.store.write() as conn: @@ -1195,22 +2026,7 @@ def review(self,goal_id,*,actor='scheduler'): if job['state']!='running' or job['lease_token']!=token: return self._receipt(conn,goal_id,self._decision(conn,goal_id)) active_options = dedup_judge.connection_settings(conn) - latest = {row['id']:_row(conn,row['id']) for row in observed} - stale = any(latest[row['id']]['revision']!=row['revision'] or latest[row['id']]['state']!='open' - or latest[row['id']]['merged_into'] is not None - or not scope.allows_write(latest[row['id']]['entry_id']) for row in observed) - visibility = Visibility(conn) - stale = stale or any(visibility.goal(row['id']) != observed_scope for row in observed) - # Subject aliases and source excerpts are not covered by goal revisions. - if not stale: - for row,snapshot in zip(observed,snapshots): - check = latest[row['id']] - check['deadline'] = _deadline(check['deadline'],role_zone(conn),strict=False) or check['deadline'] - if dedup_judge.material(conn,check)!=snapshot: - stale = True - break - if conflicts and not stale: - stale = dedup_judge.source_speakers(conn,observed)!=source_snapshot + stale = dedup_review_stale(conn,review,scope=scope) if stale: decision = {'status':'pending','target_id':None,'reason':'stale'} elif datetime.fromisoformat(job['lease_until'])<=current: @@ -1324,6 +2140,45 @@ def dismiss_duplicate(self,goal_id,other_id,*,expected_revision,other_revision,a operation(conn,'goal_duplicate_dismiss','goal',goal_id,{'other_id':other_id,'snapshot_ids':snapshot_ids},actor=actor,stamp=current.isoformat()) return _project(conn,_row(conn,goal_id),current=current) + def reopen(self,goal_id,*,expected_revision,actor='admin',reason=None): + """Reopen a goal closed by the daily automatic review (G1). + + Afterwards only messages newer than this reopen can serve as evidence, + so a later review cannot close it again on the evidence just rejected. + """ + reason = _reason(reason,'goal reopened by administrator') + with self.store.write() as conn: + current = _current(self.clock()) + row = _row(conn,goal_id) + _check(row,expected_revision) + closing = auto_closure(conn,goal_id) + if closing is None: + raise GoalError('state','只能重开每日维护自动完成或放弃的目标') + before = _snapshot(conn,goal_id) + watermark = conn.execute('SELECT COALESCE(MAX(id),0) FROM messages').fetchone()[0] + changed = conn.execute("""UPDATE goals SET state='open',closed_at=NULL,closed_by=NULL, + auto_review_after_message_id=?,revision=revision+1,updated_at=? + WHERE id=? AND revision=? AND merged_into IS NULL""", + (watermark,current.isoformat(),goal_id,row['revision'])).rowcount + if changed!=1: + raise GoalConflict() + _cancel(conn,goal_id,current) + _schedule(conn,_row(conn,goal_id),current) + # Reopening rejects this closure: its evidence is no longer kept from retention. + conn.execute('DELETE FROM goal_auto_review_evidence WHERE review_id=?',(closing['id'],)) + sid = _record_snapshot(conn,goal_id,before,actor=actor,action='reopen',reason=reason,current=current) + operation(conn,'goal_reopen','goal',goal_id,{'revision_before':row['revision'],'snapshot_id':sid, + 'review_id':closing['id'],'message_watermark':watermark},actor=actor,stamp=current.isoformat()) + return _project(conn,_row(conn,goal_id),current=current) + + def undo_auto_merge(self,judgment_id,*,expected_revision,actor='admin',reason=None): + """Undo one automatic merge by daily maintenance (G2); 409 if the survivor changed.""" + with self.store.write() as conn: + current = _current(self.clock()) + target = undo_auto_merge(conn,current,judgment_id=judgment_id,expected_revision=expected_revision, + actor=actor,reason=reason) + return _project(conn,_row(conn,target),current=current) + def clear_basis_annotation(self,goal_id,annotation_id,*,expected_revision,actor='admin',reason=None): reason = _reason(reason,'basis warning cleared by administrator') with self.store.write() as conn: @@ -1448,19 +2303,24 @@ def configure_question_expiry(self, days, *, actor='admin'): return days def configure(self,*,default_reminder_minutes=None,overdue_reminders=None, - internal_question_expiry_days=None,actor='admin'): + internal_question_expiry_days=None,goal_auto_review_enabled=None, + goal_duplicate_auto_judge_enabled=None,actor='admin'): if default_reminder_minutes is not None: _lead(default_reminder_minutes) if overdue_reminders is not None and type(overdue_reminders) is not bool: raise GoalError('overdue_reminders','过期提醒开关须为布尔值') + for key,value in ((AUTO_REVIEW_KEY,goal_auto_review_enabled),(DUPLICATE_AUTO_KEY,goal_duplicate_auto_judge_enabled)): + if value is not None and type(value) is not bool: + raise GoalError(key,'自动复核开关须为布尔值') if internal_question_expiry_days is not None: _question_expiry_days(internal_question_expiry_days) with self.store.write() as conn: current=_current(self.clock()) settings=_stored_goal_settings(conn) requested = {'default_reminder_minutes': default_reminder_minutes, - 'overdue_reminders': overdue_reminders, QUESTION_EXPIRY_KEY: internal_question_expiry_days} - changes={k:v for k,v in requested.items() if v is not None and v!=settings[k]} + 'overdue_reminders': overdue_reminders, QUESTION_EXPIRY_KEY: internal_question_expiry_days, + AUTO_REVIEW_KEY: goal_auto_review_enabled, DUPLICATE_AUTO_KEY: goal_duplicate_auto_judge_enabled} + changes={k:v for k,v in requested.items() if v is not None and v!={**AUTO_DEFAULTS,**settings}[k]} settings.update(changes) conn.execute("INSERT INTO runtime_settings(key,value_json) VALUES('goals',?) ON CONFLICT(key) DO UPDATE SET value_json=excluded.value_json",(dumps(settings),)) if 'default_reminder_minutes' in changes: diff --git a/src/iris/maintenance.py b/src/iris/maintenance.py index e13f97e0..9a727156 100644 --- a/src/iris/maintenance.py +++ b/src/iris/maintenance.py @@ -124,6 +124,9 @@ def request(self, *, trigger="manual", schedule_key=None, actor=None): config = lifecycle_settings(conn) config['media_retention_days'] = media_retention_days(self.store) config['abandoned_retry_days'] = abandoned_retry_days(conn) + from .goals import auto_review_switches + # G1/G2 switches are fixed for the run; an older run has none (off). + config['goal_auto'] = auto_review_switches(conn) from .consolidation import consolidation_settings model_config = consolidation_settings(conn) zone_row = conn.execute("SELECT value_json FROM runtime_settings WHERE key='timezone'").fetchone() @@ -202,6 +205,8 @@ def _run(self, run_id, *, stop=None): continue candidates = self._candidates(run, phase) if not candidates: + if phase == "goals" and not self._goal_auto(run, stop=stop): + return if phase == "media": orphans = cleanup_upload_orphans(self.store, self.clock()) if any(orphans.values()): @@ -216,6 +221,249 @@ def _run(self, run_id, *, stop=None): return self._process_item(run, phase, candidate) + # G line (full-auto stage): model judgments after the deterministic goal + # items. Every model call is outside a transaction; each write is one short + # transaction with its report item. A restarted run skips recorded + # fingerprints, and failures stay unrecorded so a later pass retries them. + def _goal_auto(self, run, *, stop=None): + from .goals import auto_review_switches + configured = json.loads(run["settings_json"]).get("goal_auto") or {} + with self.store.read() as conn: + live = auto_review_switches(conn) + for section, step in (("duplicates", self._goal_duplicates), ("review", self._goal_auto_review)): + if not configured.get(section): + continue + if not live[section]: + with self.store.write() as conn: + self._goal_summary(conn, run, section, {**self._goal_counts(conn, run, section), "stopped": "disabled"}) + continue + if not step(run, stop=stop): + return False + return True + + @staticmethod + def _model_unavailable(error): + return bool(error.paused or error.category in ("paused", "configuration", "account", "budget") + or error.reason in ("rate_limited", "unconfigured", "interrupted", "usage_limit")) + + def _goal_item(self, conn, run, item_key, object_id, outcome, reason, details): + conn.execute("""INSERT INTO maintenance_items(run_id,phase,item_key,memory_id,object_id,outcome,reason,details_json,created_at) + VALUES(?,'goals',?,NULL,?,?,?,?,?) ON CONFLICT(run_id,phase,item_key) DO UPDATE SET + object_id=excluded.object_id,outcome=excluded.outcome,reason=excluded.reason, + details_json=excluded.details_json,created_at=excluded.created_at""", + (run["id"], item_key, object_id, outcome, reason, dumps(details), self.clock().isoformat())) + + @staticmethod + def _goal_counts(conn, run, section): + row = conn.execute("SELECT progress_json FROM maintenance_runs WHERE id=?", (run["id"],)).fetchone() + return dict(json.loads(row[0]).get("goal_auto", {}).get(section, {})) + + @staticmethod + def _goal_summary(conn, run, section, counts): + # Counts accumulate across interrupted passes; the cursor marks the last + # item whose outcome is already counted, in the same transaction. + row = conn.execute("SELECT progress_json FROM maintenance_runs WHERE id=?", (run["id"],)).fetchone() + progress = json.loads(row[0]) + progress.setdefault("goal_auto", {})[section] = counts + conn.execute("UPDATE maintenance_runs SET progress_json=? WHERE id=? AND state='running'", + (dumps(progress), run["id"])) + + def _goal_step(self, run, section, counts, key, *, cursor, item=None, conn=None): + """Count one finished item, advance the cursor and write both atomically.""" + if key: + counts[key] = counts.get(key, 0) + 1 + counts["cursor"] = cursor + if conn is not None: + if item: + self._goal_item(conn, run, *item) + self._goal_summary(conn, run, section, counts) + return + with self.store.write() as write: + if item: + self._goal_item(write, run, *item) + self._goal_summary(write, run, section, counts) + + def _goal_stop(self, run, section, counts, reason): + counts["stopped"] = reason + with self.store.write() as conn: + self._goal_summary(conn, run, section, counts) + + @staticmethod + def _failure_reason(error): + # Safe categories only, never provider or exception text. + return (error.reason or error.category) if hasattr(error, "category") else type(error).__name__ + + def _goal_duplicates(self, run, *, stop=None): + """G2: judge each possible-duplicate pair with the shared dedup material.""" + from .goals import (DUPLICATE_AUTO_MAX_CALLS, GoalConflict, apply_duplicate_judgment, duplicate_auto_admission, + duplicate_auto_judged, duplicate_auto_material, duplicate_auto_pairs, judge_duplicate_pair) + from .models import ModelError + with self.store.read() as conn: + counts = self._goal_counts(conn, run, "duplicates") or {"pairs": 0, "calls": 0} + pairs = duplicate_auto_pairs(conn, goal_through=run["goal_through"]) + counts.pop("stopped", None) + done = tuple(counts.get("cursor") or (0, 0)) + for goal_a, goal_b in pairs: + if (goal_a, goal_b) <= done: + continue + if stop and stop(): + return False + cursor = [goal_a, goal_b] + key = f"duplicate:{goal_a}:{goal_b}" + failure = lambda error: (key, goal_a, "failed", self._failure_reason(error), # noqa: E731 + {"kind": "duplicate", "goal_ids": [goal_a, goal_b]}) + counts["pairs"] = counts.get("pairs", 0) + 1 + try: + with self.store.read() as conn: + material = duplicate_auto_material(conn, goal_a, goal_b) + judged = "skip" not in material and duplicate_auto_judged(conn, material) + except KeyError: + self._goal_step(run, "duplicates", counts, "pair_changed", cursor=cursor) + continue + except Exception as error: + self._goal_step(run, "duplicates", counts, "failed", cursor=cursor, item=failure(error)) + continue + if "skip" in material: + if material["skip"] == "dedup_judge_disabled": + counts["pairs"] -= 1 + self._goal_stop(run, "duplicates", counts, "dedup_judge_disabled") + break + self._goal_step(run, "duplicates", counts, material["skip"], cursor=cursor) + continue + if judged: + self._goal_step(run, "duplicates", counts, "already_judged", cursor=cursor) + continue + verdicts = {} + if material["partner_in_material"] and not material.get("scope_blocked"): + if counts.get("calls", 0) >= DUPLICATE_AUTO_MAX_CALLS: + self._goal_step(run, "duplicates", counts, "budget_exhausted", cursor=cursor) + continue + try: + duplicate_auto_admission(self.gateway) + except ModelError as error: + counts["pairs"] -= 1 + self._goal_stop(run, "duplicates", counts, error.reason or error.category) + break + counts["calls"] = counts.get("calls", 0) + 1 + try: + verdicts = judge_duplicate_pair(self.gateway, material) + except ModelError as error: + self._goal_step(run, "duplicates", counts, "failed", cursor=cursor, item=failure(error)) + if self._model_unavailable(error): + self._goal_stop(run, "duplicates", counts, error.reason or error.category) + break + continue + except Exception as error: + self._goal_step(run, "duplicates", counts, "failed", cursor=cursor, item=failure(error)) + continue + try: + with self.store.write() as conn: + result = apply_duplicate_judgment(conn, self.clock(), material=material, verdicts=verdicts, run_id=run["id"]) + if result is None: + self._goal_step(run, "duplicates", counts, "already_judged", cursor=cursor, conn=conn) + else: + self._goal_step(run, "duplicates", counts, result["outcome"], cursor=cursor, conn=conn, + item=(key, result["target_id"] or goal_a, "goal_duplicates_" + result["outcome"], + result["reason"], {"kind": "duplicate", **result})) + except (GoalConflict, KeyError): + self._goal_step(run, "duplicates", counts, "revision_conflict", cursor=cursor) + except Exception as error: + self._goal_step(run, "duplicates", counts, "failed", cursor=cursor, item=failure(error)) + else: + with self.store.write() as conn: + self._goal_summary(conn, run, "duplicates", counts) + return True + + def _goal_auto_review(self, run, *, stop=None): + """G1: close learning-created goals only on explicit, code-checked evidence.""" + from .goals import (AUTO_REVIEW_MAX_CALLS, GoalConflict, apply_goal_review, auto_review_admission, + auto_review_goal_ids, auto_review_judged, auto_review_material, judge_goal_review) + from .models import ModelError + as_of = datetime.fromisoformat(run["created_at"]) + with self.store.read() as conn: + counts = self._goal_counts(conn, run, "review") or {"goals": 0, "calls": 0} + # Least recently reviewed first; the order ignores this run's own + # judgments, so a resumed pass continues from the same cursor. + goals = auto_review_goal_ids(conn, goal_through=run["goal_through"], run_id=run["id"]) + # A goal that G2 merged into in this run waits for the next run, so the + # administrator can still undo the merge (undo needs its revision). + merged = {r[0] for r in conn.execute("""SELECT target_id FROM goal_duplicate_judgments + WHERE run_id=? AND outcome='merged' AND target_id IS NOT NULL""", (run["id"],))} + counts.pop("stopped", None) + done = tuple(counts.get("cursor") or ("", 0)) + for goal_id, _, reviewed in goals: + position = [reviewed, goal_id] + if tuple(position) <= done: + continue + if stop and stop(): + return False + key = f"auto_review:{goal_id}" + counts["goals"] = counts.get("goals", 0) + 1 + if goal_id in merged: + self._goal_step(run, "review", counts, "merged_this_run", cursor=position) + continue + try: + with self.store.read() as conn: + material = auto_review_material(conn, goal_id, current=as_of, message_through=run["message_through"]) + judged = material is not None and auto_review_judged(conn, material) + except KeyError: + self._goal_step(run, "review", counts, "goal_missing", cursor=position) + continue + except Exception as error: + self._goal_step(run, "review", counts, "failed", cursor=position, + item=(key, goal_id, "failed", self._failure_reason(error), {"kind": "auto_review"})) + continue + if material is None: + # No candidate message: never call the model. + self._goal_step(run, "review", counts, "no_candidates", cursor=position) + continue + if judged: + self._goal_step(run, "review", counts, "already_judged", cursor=position) + continue + if counts.get("calls", 0) >= AUTO_REVIEW_MAX_CALLS: + self._goal_step(run, "review", counts, "budget_exhausted", cursor=position) + continue + failure = lambda error: (key, goal_id, "failed", self._failure_reason(error), # noqa: E731 + {"kind": "auto_review", "goal_revision": material["revision"]}) + try: + # Pause and the daily token limit stop the pass before any request; + # nothing is recorded for this goal, so a later run judges it. + auto_review_admission(self.gateway) + except ModelError as error: + counts["goals"] -= 1 + self._goal_stop(run, "review", counts, error.reason or error.category) + break + counts["calls"] = counts.get("calls", 0) + 1 + try: + result = judge_goal_review(self.gateway, material) + except ModelError as error: + self._goal_step(run, "review", counts, "failed", cursor=position, item=failure(error)) + if self._model_unavailable(error): + self._goal_stop(run, "review", counts, error.reason or error.category) + break + continue + except Exception as error: + self._goal_step(run, "review", counts, "failed", cursor=position, item=failure(error)) + continue + try: + with self.store.write() as conn: + applied = apply_goal_review(conn, self.clock(), material=material, result=result, run_id=run["id"]) + if applied is None: + self._goal_step(run, "review", counts, "already_judged", cursor=position, conn=conn) + else: + outcome = ("goals_auto_" + applied["decision"] if applied["outcome"] == "closed" + else "goal_review_kept") + self._goal_step(run, "review", counts, applied["outcome"], cursor=position, conn=conn, + item=(key, goal_id, outcome, applied["reason"], {"kind": "auto_review", **applied})) + except (GoalConflict, KeyError): + self._goal_step(run, "review", counts, "revision_conflict", cursor=position) + except Exception as error: + self._goal_step(run, "review", counts, "failed", cursor=position, item=failure(error)) + else: + with self.store.write() as conn: + self._goal_summary(conn, run, "review", counts) + return True + def _persona_failure(self, run, error): # Persona is one atomic generation/check item. Even failure during due # selection must leave a terminal checkpoint before moving on to goals. @@ -577,6 +825,12 @@ def report(self, run_id): 'checked':progress.get('checks',{}).get('goals',0)} if not report['goal_review']['enabled']: report['goal_review']['skip_reason'] = 'disabled' + configured = report['settings'].get('goal_auto') or {} + for section, name in (('review', 'goal_auto_review'), ('duplicates', 'goal_duplicate_review')): + report[name] = {'enabled': bool(configured.get(section)), + **{k: v for k, v in progress.get('goal_auto', {}).get(section, {}).items() if k != 'cursor'}} + if not report[name]['enabled']: + report[name]['skip_reason'] = 'disabled' return report def _retention(self, run, *, stop=None): diff --git a/src/iris/memory_ops.py b/src/iris/memory_ops.py index 01af6960..e0b25ffb 100644 --- a/src/iris/memory_ops.py +++ b/src/iris/memory_ops.py @@ -292,6 +292,11 @@ def message_references(conn, message_id): ("learning_request", "SELECT 1 FROM entries WHERE learn_requested_through=? LIMIT 1"), ) reasons = [name for name, sql in checks if conn.execute(sql, (message_id,)).fetchone()] + # Evidence behind an automatic goal closure (G1) stays while its review record exists. + if conn.execute("SELECT 1 FROM sqlite_master WHERE name='goal_auto_review_evidence'").fetchone() and conn.execute( + """SELECT 1 FROM goal_auto_review_evidence ev JOIN goal_auto_reviews r ON r.id=ev.review_id + JOIN goals g ON g.id=r.goal_id WHERE ev.message_id=? LIMIT 1""", (message_id,)).fetchone(): + reasons.append('goal_auto_evidence') if conn.execute("SELECT 1 FROM sqlite_master WHERE name='entry_profile_incremental_work'").fetchone() and conn.execute( """SELECT 1 FROM purge_record_refs r JOIN entry_profile_incremental_work w ON CAST(w.id AS TEXT)=r.row_id WHERE r.table_name='entry_profile_incremental_work' AND r.ref_kind='message' AND r.ref_id=? diff --git a/src/iris/migrations/039_goal_auto_review.sql b/src/iris/migrations/039_goal_auto_review.sql new file mode 100644 index 00000000..e9d46d2c --- /dev/null +++ b/src/iris/migrations/039_goal_auto_review.sql @@ -0,0 +1,70 @@ +-- G line (full-auto stage): daily maintenance reviews learning-created goals +-- and judges "possible duplicate" pairs. Both switches default off in code; +-- this migration only adds durable judgment records, no settings or state. + +-- G1: one row per completed model judgment. The fingerprint is the goal +-- revision plus the newest message ID in the material; an unchanged fingerprint +-- is never sent to the model again. Evidence IDs are JSON (no message FK), so +-- message retention keeps working; a cleaned message shows as unavailable. +-- The model's free-text reason is never stored: it may quote message prose, +-- which privacy purges could not then reach. +CREATE TABLE goal_auto_reviews ( + id INTEGER PRIMARY KEY, + goal_id INTEGER NOT NULL REFERENCES goals(id), + goal_revision INTEGER NOT NULL, + latest_message_id INTEGER NOT NULL, + run_id INTEGER, + decision TEXT NOT NULL CHECK(decision IN ('completed','abandoned','open','invalid')), + outcome TEXT NOT NULL CHECK(outcome IN ('closed','kept_open')), + reason TEXT NOT NULL, + evidence_json TEXT NOT NULL DEFAULT '[]' CHECK(json_valid(evidence_json)), + material_ids_json TEXT NOT NULL DEFAULT '[]' CHECK(json_valid(material_ids_json)), + prompt_version TEXT NOT NULL, + revision_after INTEGER, + created_at TEXT NOT NULL, + UNIQUE(goal_id,goal_revision,latest_message_id) +); +CREATE INDEX goal_auto_reviews_goal ON goal_auto_reviews(goal_id,id); +CREATE INDEX goal_auto_reviews_closed ON goal_auto_reviews(outcome,id) WHERE outcome='closed'; +-- Qualifying evidence behind an automatic closure. Message retention keeps +-- these messages while the review row exists (memory_ops.message_references). +CREATE TABLE goal_auto_review_evidence ( + review_id INTEGER NOT NULL REFERENCES goal_auto_reviews(id), + message_id INTEGER NOT NULL, + PRIMARY KEY(review_id,message_id) +); +CREATE INDEX goal_auto_review_evidence_message ON goal_auto_review_evidence(message_id); + +-- G1 candidate messages are found by sender: a person's messages, or the +-- role's own messages within one goal's conversation entries. Without this the +-- per-goal query walks every message in the run's ID range. +CREATE INDEX messages_sender_entry ON messages(sender_subject_id,entry_id,id); + +-- An administrator reopen sticks: afterwards only messages newer than this +-- watermark can serve as candidates, so the same evidence cannot re-close it. +ALTER TABLE goals ADD COLUMN auto_review_after_message_id INTEGER; + +-- G2: one row per completed pair judgment with a fingerprint of every goal +-- revision and the newest source message in the shared dedup material. +-- Failures and timeouts are not recorded here; the pair is retried later. +CREATE TABLE goal_duplicate_judgments ( + id INTEGER PRIMARY KEY, + goal_a INTEGER NOT NULL REFERENCES goals(id), + goal_b INTEGER NOT NULL REFERENCES goals(id), + fingerprint TEXT NOT NULL, + run_id INTEGER, + method TEXT NOT NULL, + verdict TEXT CHECK(verdict IN ('same','different','uncertain')), + outcome TEXT NOT NULL CHECK(outcome IN ('merged','separated','kept')), + reason TEXT NOT NULL, + target_id INTEGER, + -- IDs and field values needed to undo an automatic merge; never prose. + merge_record_json TEXT CHECK(merge_record_json IS NULL OR json_valid(merge_record_json)), + undone_at TEXT, + undone_by TEXT, + created_at TEXT NOT NULL, + CHECK(goal_a()=>(t||(e((t={exports:{}}).exports,t),e=null),t.exports);(function `+c[r].replace(` at new `,` at `);return e.displayName&&u.includes(``)&&(u=u.replace(``,e.displayName)),u}while(1<=r&&0<=i);break}}}finally{Ee=!1,Error.prepareStackTrace=n}return(n=e?e.displayName||e.name:``)?Te(n):``}function Oe(e,t){switch(e.tag){case 26:case 27:case 5:return Te(e.type);case 16:return Te(`Lazy`);case 13:return e.child!==t&&t!==null?Te(`Suspense Fallback`):Te(`Suspense`);case 19:return Te(`SuspenseList`);case 0:case 15:return De(e.type,!1);case 11:return De(e.type.render,!1);case 1:return De(e.type,!0);case 31:return Te(`Activity`);case 30:return Te(`ViewTransition`);default:return``}}function ke(e){try{var t=``,n=null;do t+=Oe(e,n),n=e,e=e.return;while(e);return t}catch(e){return` Error generating stack: `+e.message+` `+e.stack}}var Ae=Object.prototype.hasOwnProperty,je=t.unstable_scheduleCallback,Me=t.unstable_cancelCallback,Ne=t.unstable_shouldYield,Pe=t.unstable_requestPaint,Fe=t.unstable_now,Ie=t.unstable_getCurrentPriorityLevel,Le=t.unstable_ImmediatePriority,Re=t.unstable_UserBlockingPriority,ze=t.unstable_NormalPriority,Be=t.unstable_LowPriority,Ve=t.unstable_IdlePriority,He=t.log,Ue=t.unstable_setDisableYieldValue,We=null,Ge=null;function Ke(e){if(typeof He==`function`&&Ue(e),Ge&&typeof Ge.setStrictMode==`function`)try{Ge.setStrictMode(We,e)}catch{}}var qe=Math.clz32?Math.clz32:Xe,Je=Math.log,Ye=Math.LN2;function Xe(e){return e>>>=0,e===0?32:31-(Je(e)/Ye|0)|0}var Ze=256,Qe=262144,$e=4194304;function et(e){var t=e&42;if(t!==0)return t;switch(e&-e){case 1:return 1;case 2:return 2;case 4:return 4;case 8:return 8;case 16:return 16;case 32:return 32;case 64:return 64;case 128:return 128;case 256:case 512:case 1024:case 2048:case 4096:case 8192:case 16384:case 32768:case 65536:case 131072:return e&-e;case 262144:case 524288:case 1048576:case 2097152:return e&3932160;case 4194304:case 8388608:case 16777216:case 33554432:return e&62914560;case 67108864:return 67108864;case 134217728:return 134217728;case 268435456:return 268435456;case 536870912:return 536870912;case 1073741824:return 0;default:return e}}function tt(e,t,n){var r=e.pendingLanes;if(r===0)return 0;var i=0,a=e.suspendedLanes,o=e.pingedLanes;e=e.warmLanes;var s=r&134217727;return s===0?(s=r&~a,s===0?o===0?n||(n=r&~e,n!==0&&(i=et(n))):i=et(o):i=et(s)):(r=s&~a,r===0?(o&=s,o===0?n||(n=s&~e,n!==0&&(i=et(n))):i=et(o)):i=et(r)),i===0?0:t!==0&&t!==i&&(t&a)===0&&(a=i&-i,n=t&-t,a>=n||a===32&&n&4194048)?t:i}function nt(e,t){return(e.pendingLanes&~(e.suspendedLanes&~e.pingedLanes)&t)===0}function rt(e,t){t&8&&(t|=t&32);var n=e.entangledLanes;if(n!==0)for(e=e.entanglements,n&=t;0n;n++)t.push(e);return t}function st(e,t){e.pendingLanes|=t,t!==268435456&&(e.suspendedLanes=0,e.pingedLanes=0,e.warmLanes=0)}function ct(e,t,n,r,i,a){var o=e.pendingLanes;e.pendingLanes=n,e.suspendedLanes=0,e.pingedLanes=0,e.warmLanes=0,e.expiredLanes&=n,e.entangledLanes&=n,e.errorRecoveryDisabledLanes&=n,e.shellSuspendCounter=0;var s=e.entanglements,c=e.expirationTimes,l=e.hiddenUpdates;for(n=o&~n;0=sr),ur=` `,dr=!1;function fr(e,t){switch(e){case`keyup`:return ar.indexOf(t.keyCode)!==-1;case`keydown`:return t.keyCode!==229;case`keypress`:case`mousedown`:case`focusout`:return!0;default:return!1}}function pr(e){return e=e.detail,typeof e==`object`&&`data`in e?e.data:null}var mr=!1;function hr(e,t){switch(e){case`compositionend`:return pr(t);case`keypress`:return t.which===32?(dr=!0,ur):null;case`textInput`:return e=t.data,e===ur&&dr?null:e;default:return null}}function gr(e,t){if(mr)return e===`compositionend`||!or&&fr(e,t)?(e=On(),Dn=En=Tn=null,mr=!1,e):null;switch(e){case`paste`:return null;case`keypress`:if(!(t.ctrlKey||t.altKey||t.metaKey)||t.ctrlKey&&t.altKey){if(t.char&&1=t)return{node:n,offset:t-e};e=r}a:{for(;n;){if(n.nextSibling){n=n.nextSibling;break a}n=n.parentNode}n=void 0}n=zr(n)}}function Vr(e,t){return e&&t?e===t?!0:e&&e.nodeType===3?!1:t&&t.nodeType===3?Vr(e,t.parentNode):`contains`in e?e.contains(t):e.compareDocumentPosition?!!(e.compareDocumentPosition(t)&16):!1:!1}function Hr(e){e=e!=null&&e.ownerDocument!=null&&e.ownerDocument.defaultView!=null?e.ownerDocument.defaultView:window;for(var t=Rr(e.document);t instanceof e.HTMLIFrameElement;){try{var n=typeof t.contentWindow.location.href==`string`}catch{n=!1}if(n)e=t.contentWindow;else break;t=Rr(e.document)}return t}function Ur(e){var t=e&&e.nodeName&&e.nodeName.toLowerCase();return t&&(t===`input`&&(e.type===`text`||e.type===`search`||e.type===`tel`||e.type===`url`||e.type===`password`)||t===`textarea`||e.contentEditable===`true`)}var Wr=Sn&&`documentMode`in document&&11>=document.documentMode,Gr=null,Kr=null,qr=null,Jr=!1;function Yr(e,t,n){var r=n.window===n?n.document:n.nodeType===9?n:n.ownerDocument;Jr||Gr==null||Gr!==Rr(r)||(r=Gr,`selectionStart`in r&&Ur(r)?r={start:r.selectionStart,end:r.selectionEnd}:(r=(r.ownerDocument&&r.ownerDocument.defaultView||window).getSelection(),r={anchorNode:r.anchorNode,anchorOffset:r.anchorOffset,focusNode:r.focusNode,focusOffset:r.focusOffset}),qr&&Lr(qr,r)||(qr=r,r=qf(Kr,`onSelect`),0>=o,i-=o,Gi=1<<32-qe(t)+i|n<h?(g=d,d=null):g=d.sibling;var _=p(i,d,s[h],c);if(_===null){d===null&&(d=g);break}e&&d&&_.alternate===null&&t(i,d),o=a(_,o,h),u===null?l=_:u.sibling=_,u=_,d=g}if(h===s.length)return n(i,d),G&&qi(i,h),l;if(d===null){for(;hg?(_=h,h=null):_=h.sibling;var y=p(i,h,v.value,l);if(y===null){h===null&&(h=_);break}e&&h&&y.alternate===null&&t(i,h),o=a(y,o,g),d===null?u=y:d.sibling=y,d=y,h=_}if(v.done)return n(i,h),G&&qi(i,g),u;if(h===null){for(;!v.done;g++,v=c.next())v=f(i,v.value,l),v!==null&&(o=a(v,o,g),d===null?u=v:d.sibling=v,d=v);return G&&qi(i,g),u}for(h=r(h);!v.done;g++,v=c.next())v=m(h,i,g,v.value,l),v!==null&&(e&&(_=v.alternate,_!==null&&h.delete(_.key===null?g:_.key)),o=a(v,o,g),d===null?u=v:d.sibling=v,d=v);return e&&h.forEach(function(e){return t(i,e)}),G&&qi(i,g),u}function _(e,r,a,c){if(typeof a==`object`&&a&&a.type===j&&a.key===null&&a.props.ref===void 0&&(a=a.props.children),typeof a==`object`&&a){switch(a.$$typeof){case k:a:{for(var l=a.key;r!==null;){if(r.key===l){if(l=a.type,l===j){if(r.tag===7){n(e,r.sibling),c=i(r,a.props.children),ro(c,a),c.return=e,e=c;break a}}else if(r.elementType===l||typeof l==`object`&&l&&l.$$typeof===ne&&Xa(l)===r.type){n(e,r.sibling),c=i(r,a.props),ro(c,a),c.return=e,e=c;break a}n(e,r);break}t(e,r),r=r.sibling}a.type===j?(c=Mi(a.props.children,e.mode,c,a.key),ro(c,a),c.return=e,e=c):(c=ji(a.type,a.key,a.props,null,e.mode,c),ro(c,a),c.return=e,e=c)}return o(e);case A:a:{for(l=a.key;r!==null;){if(r.key===l){if(r.tag===4&&r.stateNode.containerInfo===a.containerInfo&&r.stateNode.implementation===a.implementation){n(e,r.sibling),c=i(r,a.children||[]),c.return=e,e=c;break a}n(e,r);break}t(e,r),r=r.sibling}c=Fi(a,e.mode,c),c.return=e,e=c}return o(e);case ne:return a=Xa(a),_(e,r,a,c)}if(de(a))return h(e,r,a,c);if(ce(a)){if(l=ce(a),typeof l!=`function`)throw Error(s(150));return a=l.call(a),g(e,r,a,c)}if(typeof a.then==`function`)return _(e,r,no(a),c);if(a.$$typeof===P)return _(e,r,xa(e,a),c);io(e,a)}return typeof a==`string`&&a!==``||typeof a==`number`||typeof a==`bigint`?(a=``+a,r!==null&&r.tag===6?(n(e,r.sibling),c=i(r,a),c.return=e,e=c):(n(e,r),c=Ni(a,e.mode,c),c.return=e,e=c),o(e)):n(e,r)}return function(e,t,n,r){try{to=0;var i=_(e,t,n,r);return eo=null,i}catch(t){if(t===Wa||t===Ka)throw t;var a=Di(29,t,null,e.mode);return a.lanes=r,a.return=e,a}}}var oo=ao(!0),so=ao(!1),co=!1;function lo(e){e.updateQueue={baseState:e.memoizedState,firstBaseUpdate:null,lastBaseUpdate:null,shared:{pending:null,lanes:0,hiddenCallbacks:null},callbacks:null}}function uo(e,t){e=e.updateQueue,t.updateQueue===e&&(t.updateQueue={baseState:e.baseState,firstBaseUpdate:e.firstBaseUpdate,lastBaseUpdate:e.lastBaseUpdate,shared:e.shared,callbacks:null})}function fo(e){return{lane:e,tag:0,payload:null,callback:null,next:null}}function po(e,t,n){var r=e.updateQueue;if(r===null)return null;if(r=r.shared,J&2){var i=r.pending;return i===null?t.next=t:(t.next=i.next,i.next=t),r.pending=t,t=wi(e),Ci(e,null,n),t}return bi(e,r,t,n),wi(e)}function mo(e,t,n){if(t=t.updateQueue,t!==null&&(t=t.shared,n&4194048)){var r=t.lanes;r&=e.pendingLanes,n|=r,t.lanes=n,ut(e,n)}}function ho(e,t){var n=e.updateQueue,r=e.alternate;if(r!==null&&(r=r.updateQueue,n===r)){var i=null,a=null;if(n=n.firstBaseUpdate,n!==null){do{var o={lane:n.lane,tag:n.tag,payload:n.payload,callback:null,next:null};a===null?i=a=o:a=a.next=o,n=n.next}while(n!==null);a===null?i=a=t:a=a.next=t}else i=a=t;n={baseState:r.baseState,firstBaseUpdate:i,lastBaseUpdate:a,shared:r.shared,callbacks:r.callbacks},e.updateQueue=n;return}e=n.lastBaseUpdate,e===null?n.firstBaseUpdate=t:e.next=t,n.lastBaseUpdate=t}var go=!1;function _o(){if(go){var e=Fa;if(e!==null)throw e}}function vo(e,t,n,r){go=!1;var i=e.updateQueue;co=!1;var a=i.firstBaseUpdate,o=i.lastBaseUpdate,s=i.shared.pending;if(s!==null){i.shared.pending=null;var c=s,l=c.next;c.next=null,o===null?a=l:o.next=l,o=c;var u=e.alternate;u!==null&&(u=u.updateQueue,s=u.lastBaseUpdate,s!==o&&(s===null?u.firstBaseUpdate=l:s.next=l,u.lastBaseUpdate=c))}if(a!==null){var d=i.baseState;o=0,u=l=c=null,s=a;do{var f=s.lane&-536870913,p=f!==s.lane;if(p?(X&f)===f:(r&f)===f){f!==0&&f===Pa&&(go=!0),u!==null&&(u=u.next={lane:0,tag:s.tag,payload:s.payload,callback:null,next:null});a:{var m=e,h=s;f=t;var g=n;switch(h.tag){case 1:if(m=h.payload,typeof m==`function`){d=m.call(g,d,f);break a}d=m;break a;case 3:m.flags=m.flags&-65537|128;case 0:if(m=h.payload,f=typeof m==`function`?m.call(g,d,f):m,f==null)break a;d=D({},d,f);break a;case 2:co=!0}}f=s.callback,f!==null&&(e.flags|=64,p&&(e.flags|=8192),p=i.callbacks,p===null?i.callbacks=[f]:p.push(f))}else p={lane:f,tag:s.tag,payload:s.payload,callback:s.callback,next:null},u===null?(l=u=p,c=d):u=u.next=p,o|=f;if(s=s.next,s===null){if(s=i.shared.pending,s===null)break;p=s,s=p.next,p.next=null,i.lastBaseUpdate=p,i.shared.pending=null}}while(1);u===null&&(c=d),i.baseState=c,i.firstBaseUpdate=l,i.lastBaseUpdate=u,a===null&&(i.shared.lanes=0),ad|=o,e.lanes=o,e.memoizedState=d}}function yo(e,t){if(typeof e!=`function`)throw Error(s(191,e));e.call(t)}function bo(e,t){var n=e.callbacks;if(n!==null)for(e.callbacks=null,e=0;ea?a:8;var o=z.T,s={};s.types=o===null?null:o.types,z.T=s,sc(e,!1,t,n);try{var c=i(),l=z.S;l!==null&&l(s,c),typeof c==`object`&&c&&typeof c.then==`function`?oc(e,t,Ra(c,r),Ad(e)):oc(e,t,r,Ad(e))}catch(n){oc(e,t,{then:function(){},status:`rejected`,reason:n},Ad())}finally{B.p=a,o!==null&&s.types!==null&&(o.types=s.types),z.T=o}}function Xs(){}function Zs(e,t,n,r){if(e.tag!==5)throw Error(s(476));var i=Qs(e).queue;Ys(e,i,t,V,n===null?Xs:function(){return $s(e),n(r)})}function Qs(e){var t=e.memoizedState;if(t!==null)return t;t={memoizedState:V,baseState:V,baseQueue:null,queue:{pending:null,lanes:0,dispatch:null,lastRenderedReducer:cs,lastRenderedState:V},next:null};var n={};return t.next={memoizedState:n,baseState:n,baseQueue:null,queue:{pending:null,lanes:0,dispatch:null,lastRenderedReducer:cs,lastRenderedState:n},next:null},e.memoizedState=t,e=e.alternate,e!==null&&(e.memoizedState=t),t}function $s(e){var t=Qs(e);t.next===null&&(t=e.alternate.memoizedState),oc(e,t.next.queue,{},Ad())}function ec(){return ba(sh)}function tc(){return rs().memoizedState}function nc(){return rs().memoizedState}function rc(e){for(var t=e.return;t!==null;){switch(t.tag){case 24:case 3:var n=Ad();e=fo(n);var r=po(t,e,n);r!==null&&(Nd(r,t,n),mo(r,t,n)),t={cache:Da()},e.payload=t;return}t=t.return}}function ic(e,t,n){var r=Ad();n={lane:r,revertLane:0,gesture:null,action:n,hasEagerState:!1,eagerState:null,next:null},cc(e)?lc(t,n):(n=xi(e,t,n,r),n!==null&&(Nd(n,e,r),uc(n,t,r)))}function ac(e,t,n){oc(e,t,n,Ad())}function oc(e,t,n,r){var i={lane:r,revertLane:0,gesture:null,action:n,hasEagerState:!1,eagerState:null,next:null};if(cc(e))lc(t,i);else{var a=e.alternate;if(e.lanes===0&&(a===null||a.lanes===0)&&(a=t.lastRenderedReducer,a!==null))try{var o=t.lastRenderedState,s=a(o,n);if(i.hasEagerState=!0,i.eagerState=s,Ir(s,o))return bi(e,t,i,0),Qu===null&&yi(),!1}catch{}if(n=xi(e,t,i,r),n!==null)return Nd(n,e,r),uc(n,t,r),!0}return!1}function sc(e,t,n,r){if(r={lane:2,revertLane:Nf(),gesture:null,action:r,hasEagerState:!1,eagerState:null,next:null},cc(e)){if(t)throw Error(s(479))}else t=xi(e,n,r,2),t!==null&&Nd(t,e,2)}function cc(e){var t=e.alternate;return e===K||t!==null&&t===K}function lc(e,t){Vo=Bo=!0;var n=e.pending;n===null?t.next=t:(t.next=n.next,n.next=t),e.pending=t}function uc(e,t,n){if(n&4194048){var r=t.lanes;r&=e.pendingLanes,n|=r,t.lanes=n,ut(e,n)}}var dc={readContext:ba,use:os,useCallback:qo,useContext:qo,useEffect:qo,useImperativeHandle:qo,useLayoutEffect:qo,useInsertionEffect:qo,useMemo:qo,useReducer:qo,useRef:qo,useState:qo,useDebugValue:qo,useDeferredValue:qo,useTransition:qo,useSyncExternalStore:qo,useId:qo,useHostTransitionStatus:qo,useFormState:qo,useActionState:qo,useOptimistic:qo,useMemoCache:qo,useCacheRefresh:qo,useEffectEvent:qo},fc={readContext:ba,use:os,useCallback:function(e,t){return ns().memoizedState=[e,t===void 0?null:t],e},useContext:ba,useEffect:Is,useImperativeHandle:function(e,t,n){n=n==null?null:n.concat([e]),Ps(4194308,4,Hs.bind(null,t,e),n)},useLayoutEffect:function(e,t){return Ps(4194308,4,e,t)},useInsertionEffect:function(e,t){Ps(4,2,e,t)},useMemo:function(e,t){var n=ns();t=t===void 0?null:t;var r=e();if(Ho){Ke(!0);try{e()}finally{Ke(!1)}}return n.memoizedState=[r,t],r},useReducer:function(e,t,n){var r=ns();if(n!==void 0){var i=n(t);if(Ho){Ke(!0);try{n(t)}finally{Ke(!1)}}}else i=t;return r.memoizedState=r.baseState=i,e={pending:null,lanes:0,dispatch:null,lastRenderedReducer:e,lastRenderedState:i},r.queue=e,e=e.dispatch=ic.bind(null,K,e),[r.memoizedState,e]},useRef:function(e){var t=ns();return e={current:e},t.memoizedState=e},useState:function(e){e=vs(e);var t=e.queue,n=ac.bind(null,K,t);return t.dispatch=n,[e.memoizedState,n]},useDebugValue:Ws,useDeferredValue:function(e,t){return qs(ns(),e,t)},useTransition:function(){var e=vs(!1);return e=Ys.bind(null,K,e.queue,!0,!1),ns().memoizedState=e,[!1,e]},useSyncExternalStore:function(e,t,n){var r=K,i=ns();if(G){if(n===void 0)throw Error(s(407));n=n()}else{if(n=t(),Qu===null)throw Error(s(349));X&127||ps(r,t,n)}i.memoizedState=n;var a={value:n,getSnapshot:t};return i.queue=a,Is(hs.bind(null,r,a,e),[e]),r.flags|=2048,Ms(9,{destroy:void 0},ms.bind(null,r,a,n,t),null),n},useId:function(){var e=ns(),t=Qu.identifierPrefix;if(G){var n=Ki,r=Gi;n=(r&~(1<<32-qe(r)-1)).toString(32)+n,t=`_`+t+`R_`+n,n=Uo++,0<\/script>`,a=a.removeChild(a.firstChild);break;case`select`:a=typeof r.is==`string`?o.createElement(`select`,{is:r.is}):o.createElement(`select`),r.multiple?a.multiple=!0:r.size&&(a.size=r.size);break;default:a=typeof r.is==`string`?o.createElement(i,{is:r.is}):o.createElement(i)}}a[_t]=t,a[vt]=r;a:for(o=t.child;o!==null;){if(o.tag===5||o.tag===6)a.appendChild(o.stateNode);else if(o.tag!==4&&o.tag!==27&&o.child!==null){o.child.return=o,o=o.child;continue}if(o===t)break a;for(;o.sibling===null;){if(o.return===null||o.return===t)break a;o=o.return}o.sibling.return=o.return,o=o.sibling}t.stateNode=a;a:switch(np(a,i,r),i){case`button`:case`input`:case`select`:case`textarea`:r=!!r.autoFocus;break a;case`img`:r=!0;break a;default:r=!1}r&&ul(t)}}return hl(t),t.subtreeFlags&=-33554433,dl(t,t.type,e===null?null:e.memoizedProps,t.pendingProps,n),null;case 6:if(e&&t.stateNode!=null)e.memoizedProps!==r&&ul(t);else{if(typeof r!=`string`&&t.stateNode===null)throw Error(s(166));if(e=_e.current,oa(t)){if(e=t.stateNode,n=t.memoizedProps,r=null,i=Qi,i!==null)switch(i.tag){case 27:case 5:r=i.memoizedProps}e[_t]=t,e=!!(e.nodeValue===n||r!==null&&!0===r.suppressHydrationWarning||$f(e.nodeValue,n)),e||ra(t,!0)}else e=lp(e).createTextNode(r),e[_t]=t,t.stateNode=e}return hl(t),null;case 31:if(n=t.memoizedState,e===null||e.memoizedState!==null){if(r=oa(t),n!==null){if(e===null){if(!r)throw Error(s(318));if(e=t.memoizedState,e=e===null?null:e.dehydrated,!e)throw Error(s(557));e[_t]=t}else sa(),!(t.flags&128)&&(t.memoizedState=null),t.flags|=4;hl(t),e=!1}else n=ca(),e!==null&&e.memoizedState!==null&&(e.memoizedState.hydrationErrors=n),e=!0;if(!e)return t.flags&256?(Mo(t),t):(Mo(t),null);if(t.flags&128)throw Error(s(558))}return hl(t),null;case 13:if(r=t.memoizedState,e===null||e.memoizedState!==null&&e.memoizedState.dehydrated!==null){if(i=oa(t),r!==null&&r.dehydrated!==null){if(e===null){if(!i)throw Error(s(318));if(i=t.memoizedState,i=i===null?null:i.dehydrated,!i)throw Error(s(317));i[_t]=t}else sa(),!(t.flags&128)&&(t.memoizedState=null),t.flags|=4;hl(t),i=!1}else i=ca(),e!==null&&e.memoizedState!==null&&(e.memoizedState.hydrationErrors=i),i=!0;if(!i)return t.flags&256?(Mo(t),t):(Mo(t),null)}return Mo(t),t.flags&128?(t.lanes=n,t):(n=r!==null,e=e!==null&&e.memoizedState!==null,n&&(r=t.child,i=null,r.alternate!==null&&r.alternate.memoizedState!==null&&r.alternate.memoizedState.cachePool!==null&&(i=r.alternate.memoizedState.cachePool.pool),a=null,r.memoizedState!==null&&r.memoizedState.cachePool!==null&&(a=r.memoizedState.cachePool.pool),a!==i&&(r.flags|=2048)),n!==e&&n&&(t.child.flags|=8192),pl(t,t.updateQueue),hl(t),null);case 4:return be(),e===null&&Uf(t.stateNode.containerInfo),t.flags|=67108864,hl(t),null;case 10:return ma(t.type),hl(t),null;case 19:if(Fo(t),r=t.memoizedState,r===null)return hl(t),null;if(i=!!(t.flags&128),a=r.rendering,a===null){if(i)ml(r,!1);else{if(id!==0||e!==null&&e.flags&128)for(e=t.child;e!==null;){if(a=Io(e),a!==null){for(t.flags|=128,ml(r,!1),e=a.updateQueue,t.updateQueue=e,pl(t,e),t.subtreeFlags=0,e=n,n=t.child;n!==null;)Ai(n,e),n=n.sibling;return Po(t,No.current&1|2),G&&qi(t,r.treeForkCount),t.child}e=e.sibling}r.tail!==null&&Fe()>hd&&(t.flags|=128,i=!0,ml(r,!1),t.lanes=4194304)}}else{if(!i){if(e=Io(a),e!==null){if(t.flags|=128,i=!0,e=e.updateQueue,t.updateQueue=e,pl(t,e),ml(r,!0),r.tail===null&&r.tailMode!==`collapsed`&&r.tailMode!==`visible`&&!a.alternate&&!G)return hl(t),null}else 2*Fe()-r.renderingStartTime>hd&&n!==536870912&&(t.flags|=128,i=!0,ml(r,!1),t.lanes=4194304)}r.isBackwards?(a.sibling=t.child,t.child=a):(e=r.last,e===null?t.child=a:e.sibling=a,r.last=a)}if(r.tail!==null){e=r.tail;a:{for(n=e;n!==null;){if(n.alternate!==null){n=!1;break a}n=n.sibling}n=!0}return r.rendering=e,r.tail=e.sibling,r.renderingStartTime=Fe(),e.sibling=null,a=No.current,a=i?a&1|2:a&1,r.tailMode===`visible`||r.tailMode===`collapsed`||!n||G?Po(t,a):(n=a,H(Eo,t),H(No,n),Do===null&&(Do=t)),G&&qi(t,r.treeForkCount),e}return hl(t),null;case 22:case 23:return Mo(t),To(),r=t.memoizedState!==null,e===null?r&&(t.flags|=8192):e.memoizedState!==null!==r&&(t.flags|=8192),r?n&536870912&&!(t.flags&128)&&(hl(t),t.subtreeFlags&6&&(t.flags|=8192)):hl(t),n=t.updateQueue,n!==null&&pl(t,n.retryQueue),n=null,e!==null&&e.memoizedState!==null&&e.memoizedState.cachePool!==null&&(n=e.memoizedState.cachePool.pool),r=null,t.memoizedState!==null&&t.memoizedState.cachePool!==null&&(r=t.memoizedState.cachePool.pool),r!==n&&(t.flags|=2048),e!==null&&he(Ba),null;case 24:return n=null,e!==null&&(n=e.memoizedState.cache),t.memoizedState.cache!==n&&(t.flags|=2048),ma(Ea),hl(t),null;case 25:return null;case 30:return t.flags|=33554432,hl(t),null}throw Error(s(156,t.tag))}function _l(e,t){switch(Xi(t),t.tag){case 1:return e=t.flags,e&65536?(t.flags=e&-65537|128,t):null;case 3:return ma(Ea),be(),e=t.flags,e&65536&&!(e&128)?(t.flags=e&-65537|128,t):null;case 26:case 27:case 5:return Se(t),null;case 31:if(t.memoizedState!==null){if(Mo(t),t.alternate===null)throw Error(s(340));sa()}return e=t.flags,e&65536?(t.flags=e&-65537|128,t):null;case 13:if(Mo(t),e=t.memoizedState,e!==null&&e.dehydrated!==null){if(t.alternate===null)throw Error(s(340));sa()}return e=t.flags,e&65536?(t.flags=e&-65537|128,t):null;case 19:return Fo(t),e=t.flags,e&65536?(t.flags=e&-65537|128,e=t.memoizedState,e!==null&&(e.rendering=null,e.tail=null),t.flags|=4,t):null;case 4:return be(),null;case 10:return ma(t.type),null;case 22:case 23:return Mo(t),To(),e!==null&&he(Ba),e=t.flags,e&65536?(t.flags=e&-65537|128,t):null;case 24:return ma(Ea),null;case 25:return null;default:return null}}function vl(e,t){switch(Xi(t),t.tag){case 3:ma(Ea),be();break;case 26:case 27:case 5:Se(t);break;case 4:be();break;case 31:t.memoizedState!==null&&Mo(t);break;case 13:Mo(t);break;case 19:Fo(t);break;case 10:ma(t.type);break;case 22:case 23:Mo(t),To(),e!==null&&he(Ba);break;case 24:ma(Ea)}}function yl(e,t){try{var n=t.updateQueue,r=n===null?null:n.lastEffect;if(r!==null){var i=r.next;n=i;do{if((n.tag&e)===e){r=void 0;var a=n.create,o=n.inst;r=a(),o.destroy=r}n=n.next}while(n!==i)}}catch(e){Q(t,t.return,e)}}function bl(e,t,n){try{var r=t.updateQueue,i=r===null?null:r.lastEffect;if(i!==null){var a=i.next;r=a;do{if((r.tag&e)===e){var o=r.inst,s=o.destroy;if(s!==void 0){o.destroy=void 0,i=t;var c=n,l=s;try{l()}catch(e){Q(i,c,e)}}}r=r.next}while(r!==a)}}catch(e){Q(t,t.return,e)}}function xl(e){var t=e.updateQueue;if(t!==null){var n=e.stateNode;try{bo(t,n)}catch(t){Q(e,e.return,t)}}}function Sl(e,t,n){n.props=yc(e.type,e.memoizedProps),n.state=e.memoizedState;try{n.componentWillUnmount()}catch(n){Q(e,t,n)}}function Cl(e,t){try{var n=e.ref;if(n!==null){switch(e.tag){case 26:case 27:case 5:var r=e.stateNode;break;case 30:var i=e.stateNode,a=fi(e.memoizedProps,i);(i.ref===null||i.ref.name!==a)&&(i.ref=Pp(a)),r=i.ref;break;case 7:if(e.stateNode===null){var o=new Fp(e);h(e.child,!1,Qp,o,void 0,void 0),e.stateNode=o}r=e.stateNode;break;default:r=e.stateNode}typeof n==`function`?e.refCleanup=n(r):n.current=r}}catch(n){Q(e,t,n)}}function wl(e,t){var n=e.ref,r=e.refCleanup;if(n!==null){if(typeof r==`function`)try{r()}catch(n){Q(e,t,n)}finally{e.refCleanup=null,e=e.alternate,e!=null&&(e.refCleanup=null)}else if(typeof n==`function`)try{n(null)}catch(n){Q(e,t,n)}else n.current=null}}function Tl(e,t){if((e.tag===5||e.tag===27||e.tag===6)&&e.alternate===null&&t!==null)for(var n=0;n title`))),np(r,t,n),r[_t]=e,jt(r),t=r;break a;case`link`:if(a=Gm(`link`,`href`,i).get(t+(n.href||``))){for(o=0;og&&(o=g,g=h,h=o);var _=Br(s,h),v=Br(s,g);if(_&&v&&(p.rangeCount!==1||p.anchorNode!==_.node||p.anchorOffset!==_.offset||p.focusNode!==v.node||p.focusOffset!==v.offset)){var y=d.createRange();y.setStart(_.node,_.offset),p.removeAllRanges(),h>g?(p.addRange(y),p.extend(v.node,v.offset)):(y.setEnd(v.node,v.offset),p.addRange(y))}}}}for(d=[],p=s;p=p.parentNode;)p.nodeType===1&&d.push({element:p,left:p.scrollLeft,top:p.scrollTop});for(typeof s.focus==`function`&&s.focus(),s=0;sn?32:n,z.T=null,n=Cd,Cd=null;var a=yd,o=xd;if(vd=0,bd=yd=null,xd=0,J&6)throw Error(s(331));var c=J;if(J|=4,qu(a.current),zu(a,a.current,o,n),J=c,Ef(0,!1),Ge&&typeof Ge.onPostCommitFiberRoot==`function`)try{Ge.onPostCommitFiberRoot(We,a)}catch{}return!0}finally{B.p=i,z.T=r,lf(e,t)}}function ff(e,t,n){t=Li(n,t),t=Tc(e.stateNode,t,2),e=po(e,t,2),e!==null&&(st(e,2),Tf(e))}function Q(e,t,n){if(e.tag===3)ff(e,e,n);else for(;t!==null;){if(t.tag===3){ff(t,e,n);break}if(t.tag===1){var r=t.stateNode;if(typeof t.type.getDerivedStateFromError==`function`||typeof r.componentDidCatch==`function`&&(_d===null||!_d.has(r))){e=Li(n,e),n=Ec(2),r=po(t,n,2),r!==null&&(Dc(n,r,t,e),st(r,2),Tf(r));break}}t=t.return}}function pf(e,t,n){var r=e.pingCache;if(r===null){r=e.pingCache=new Zu;var i=new Set;r.set(t,i)}else i=r.get(t),i===void 0&&(i=new Set,r.set(t,i));i.has(n)||(nd=!0,i.add(n),e=mf.bind(null,e,t,n),t.then(e,e))}function mf(e,t,n){var r=e.pingCache;r!==null&&r.delete(t),e.pingedLanes|=e.suspendedLanes&n,e.warmLanes&=~n,Qu===e&&(X&n)===n&&(id===4||id===3&&(X&62914560)===X&&300>Fe()-pd?J&2?sd|=n:Bd(e,0):sd|=n,ld===X&&(ld=0)),Tf(e)}function hf(e,t){t===0&&(t=at()),e=Si(e,t),e!==null&&(st(e,t),Tf(e))}function gf(e){var t=e.memoizedState,n=0;t!==null&&(n=t.retryLane),hf(e,n)}function _f(e,t){var n=0;switch(e.tag){case 31:case 13:var r=e.stateNode,i=e.memoizedState;i!==null&&(n=i.retryLane);break;case 19:r=e.stateNode;break;case 22:r=e.stateNode._retryCache;break;default:throw Error(s(314))}r!==null&&r.delete(t),hf(e,n)}function vf(e,t){return je(e,t)}var yf=null,bf=null,xf=!1,Sf=!1,Cf=!1,wf=0;function Tf(e){e!==bf&&e.next===null&&(bf===null?yf=bf=e:bf=bf.next=e),Sf=!0,xf||(xf=!0,Mf())}function Ef(e,t){if(!Cf&&Sf){Cf=!0;do for(var n=!1,r=yf;r!==null;){if(!t){if(e!==0){var i=r.pendingLanes;if(i===0)var a=0;else{var o=r.suspendedLanes,s=r.pingedLanes;a=(1<<31-qe(42|e)+1)-1,a&=i&~(o&~s),a=a&201326741?a&201326741|1:a?a|2:0}a!==0&&(n=!0,jf(r,a))}else a=X,a=tt(r,r===Qu?a:0,r.cancelPendingCommit!==null||r.timeoutHandle!==-1),!(a&3)||nt(r,a)||(n=!0,jf(r,a))}r=r.next}while(n);Cf=!1}}function Df(){Of()}function Of(){Sf=xf=!1;var e=0;wf!==0&&hp()&&(e=wf);for(var t=Fe(),n=null,r=yf;r!==null;){var i=r.next,a=kf(r,t);a===0?(r.next=null,n===null?yf=i:n.next=i,i===null&&(bf=n)):(n=r,(e!==0||a&3)&&(Sf=!0)),r=i}vd!==0&&vd!==5||Ef(e,!1),wf!==0&&(wf=0)}function kf(e,t){for(var n=e.suspendedLanes,r=e.pingedLanes,i=e.expirationTimes,a=e.pendingLanes&-62914561;0s)break;var u=c.transferSize,d=c.initiatorType;u&&ap(d)&&(c=c.responseEnd,o+=u*(c$m){s.length=o;break}f=new Promise(jp.bind(f)),s.push(f)}}}if(0`u`?null:document;function Tm(e,t,n){var r=wm;if(r&&typeof t==`string`&&t){var i=Zt(t);i=`link[rel="`+e+`"][href="`+i+`"]`,typeof n==`string`&&(i+=`[crossorigin="`+n+`"]`),ym.has(i)||(ym.add(i),e={rel:e,crossOrigin:n,href:t},r.querySelector(i)===null&&(t=r.createElement(`link`),np(t,`link`,e),jt(t),r.head.appendChild(t)))}}function Em(e){xm.D(e),Tm(`dns-prefetch`,e,null)}function Dm(e,t){xm.C(e,t),Tm(`preconnect`,e,t)}function Om(e,t,n){xm.L(e,t,n);var r=wm;if(r&&e&&t){var i=`link[rel="preload"][as="`+Zt(t)+`"]`;t===`image`&&n&&n.imageSrcSet?(i+=`[imagesrcset="`+Zt(n.imageSrcSet)+`"]`,typeof n.imageSizes==`string`&&(i+=`[imagesizes="`+Zt(n.imageSizes)+`"]`)):i+=`[href="`+Zt(e)+`"]`;var a=i;switch(t){case`style`:a=Pm(e);break;case`script`:a=Rm(e)}if(!(vm.has(a)||(e=D({rel:`preload`,href:t===`image`&&n&&n.imageSrcSet?void 0:e,as:t},n),vm.set(a,e),r.querySelector(i)!==null||t===`style`&&r.querySelector(Fm(a))||t===`script`&&r.querySelector(zm(a))))){var o=r.createElement(`link`);np(o,`link`,e),t===`style`&&(o[Tt]=!0,o.onload=o.onerror=function(){Mt(o)}),jt(o),r.head.appendChild(o)}}}function km(e,t){xm.m(e,t);var n=wm;if(n&&e){var r=t&&typeof t.as==`string`?t.as:`script`,i=`link[rel="modulepreload"][as="`+Zt(r)+`"][href="`+Zt(e)+`"]`,a=i;switch(r){case`audioworklet`:case`paintworklet`:case`serviceworker`:case`sharedworker`:case`worker`:case`script`:a=Rm(e)}if(!vm.has(a)&&(e=D({rel:`modulepreload`,href:e},t),vm.set(a,e),n.querySelector(i)===null)){switch(r){case`audioworklet`:case`paintworklet`:case`serviceworker`:case`sharedworker`:case`worker`:case`script`:if(n.querySelector(zm(a)))return}r=n.createElement(`link`),np(r,`link`,e),jt(r),n.head.appendChild(r)}}}function Am(e,t,n){xm.S(e,t,n);var r=wm;if(r&&e){var i=At(r).hoistableStyles,a=Pm(e);t||=`default`;var o=i.get(a);if(!o){var s={loading:0,preload:null};if(o=r.querySelector(Fm(a)))s.loading=5;else{e=D({rel:`stylesheet`,href:e,"data-precedence":t},n),(n=vm.get(a))&&Hm(e,n);var c=o=r.createElement(`link`);jt(c),np(c,`link`,e),c._p=new Promise(function(e,t){c.onload=e,c.onerror=t}),c.addEventListener(`load`,function(){s.loading|=1}),c.addEventListener(`error`,function(){s.loading|=2}),s.loading|=4,Vm(o,t,r)}o={type:`stylesheet`,instance:o,count:1,state:s},i.set(a,o)}}}function jm(e,t){xm.X(e,t);var n=wm;if(n&&e){var r=At(n).hoistableScripts,i=Rm(e),a=r.get(i);a||(a=n.querySelector(zm(i)),a||(e=D({src:e,async:!0},t),(t=vm.get(i))&&Um(e,t),a=n.createElement(`script`),jt(a),np(a,`link`,e),n.head.appendChild(a)),a={type:`script`,instance:a,count:1,state:null},r.set(i,a))}}function Mm(e,t){xm.M(e,t);var n=wm;if(n&&e){var r=At(n).hoistableScripts,i=Rm(e),a=r.get(i);a||(a=n.querySelector(zm(i)),a||(e=D({src:e,async:!0,type:`module`},t),(t=vm.get(i))&&Um(e,t),a=n.createElement(`script`),jt(a),np(a,`link`,e),n.head.appendChild(a)),a={type:`script`,instance:a,count:1,state:null},r.set(i,a))}}function Nm(e,t,n,r){var i=(i=_e.current)?bm(i):null;if(!i)throw Error(s(446));switch(e){case`meta`:case`title`:return null;case`style`:return typeof n.precedence==`string`&&typeof n.href==`string`?(n=Pm(n.href),t=At(i).hoistableStyles,r=t.get(n),r||(r={type:`style`,instance:null,count:0,state:null},t.set(n,r)),r):{type:`void`,instance:null,count:0,state:null};case`link`:if(n.rel===`stylesheet`&&typeof n.href==`string`&&typeof n.precedence==`string`){e=Pm(n.href);var a=At(i).hoistableStyles,o=a.get(e);if(o||(i=i.ownerDocument||i,o={type:`stylesheet`,instance:null,count:0,state:{loading:0,preload:null}},a.set(e,o),(a=i.querySelector(Fm(e)))?a._p||(o.instance=a,o.state.loading=5):(a=vm.get(e),a||(a={rel:`preload`,as:`style`,href:n.href,crossOrigin:n.crossOrigin,integrity:n.integrity,media:n.media,hrefLang:n.hrefLang,referrerPolicy:n.referrerPolicy},vm.set(e,a)),Lm(i,e,a,o.state))),t&&r===null)throw Error(s(528,``));return o}if(t&&r!==null)throw Error(s(529,``));return null;case`script`:return t=n.async,n=n.src,typeof n==`string`&&t&&typeof t!=`function`&&typeof t!=`symbol`?(n=Rm(n),t=At(i).hoistableScripts,r=t.get(n),r||(r={type:`script`,instance:null,count:0,state:null},t.set(n,r)),r):{type:`void`,instance:null,count:0,state:null};default:throw Error(s(444,e))}}function Pm(e){return`href="`+Zt(e)+`"`}function Fm(e){return`link[rel="stylesheet"][`+e+`]`}function Im(e){return D({},e,{"data-precedence":e.precedence,precedence:null})}function Lm(e,t,n,r){if(t=e.querySelector(`link[rel="preload"][as="style"][`+t+`]`)){if(!0!==t[Tt]){r.loading=1;return}}else t=e.createElement(`link`),t[Tt]=!0,t.onload=t.onerror=Mt.bind(null,t),np(t,`link`,n),jt(t),e.head.appendChild(t);r.preload=t,t.addEventListener(`load`,function(){return r.loading|=1}),t.addEventListener(`error`,function(){return r.loading|=2})}function Rm(e){return`[src="`+Zt(e)+`"]`}function zm(e){return`script[async]`+e}function Bm(e,t,n){if(t.count++,t.instance===null)switch(t.type){case`style`:var r=e.querySelector(`style[data-href~="`+Zt(n.href)+`"]`);if(r)return t.instance=r,jt(r),r;var i=D({},n,{"data-href":n.href,"data-precedence":n.precedence,href:null,precedence:null});return r=(e.ownerDocument||e).createElement(`style`),jt(r),np(r,`style`,i),Vm(r,n.precedence,e),t.instance=r;case`stylesheet`:i=Pm(n.href);var a=e.querySelector(Fm(i));if(a)return t.state.loading|=4,t.instance=a,jt(a),a;r=Im(n),(i=vm.get(i))&&Hm(r,i),a=(e.ownerDocument||e).createElement(`link`),jt(a);var o=a;return o._p=new Promise(function(e,t){o.onload=e,o.onerror=t}),np(a,`link`,r),t.state.loading|=4,Vm(a,n.precedence,e),t.instance=a;case`script`:return a=Rm(n.src),(i=e.querySelector(zm(a)))?(t.instance=i,jt(i),i):(r=n,(i=vm.get(a))&&(r=D({},n),Um(r,i)),e=e.ownerDocument||e,i=e.createElement(`script`),jt(i),np(i,`link`,r),e.head.appendChild(i),t.instance=i);case`void`:return null;default:throw Error(s(443,t.type))}else t.type===`stylesheet`&&!(t.state.loading&4)&&(r=t.instance,t.state.loading|=4,Vm(r,n.precedence,e));return t.instance}function Vm(e,t,n){for(var r=n.querySelectorAll(`link[rel="stylesheet"][data-precedence],style[data-precedence]`),i=r.length?r[r.length-1]:null,a=i,o=0;o title`):null)}function qm(e,t,n){if(n===1||t.itemProp!=null)return!1;switch(e){case`meta`:case`title`:return!0;case`style`:if(typeof t.precedence!=`string`||typeof t.href!=`string`||t.href===``)break;return!0;case`link`:if(typeof t.rel!=`string`||typeof t.href!=`string`||t.href===``||t.onLoad||t.onError)break;switch(t.rel){case`stylesheet`:return e=t.disabled,typeof t.precedence==`string`&&e==null;default:return!0}case`script`:if(t.async&&typeof t.async!=`function`&&typeof t.async!=`symbol`&&!t.onLoad&&!t.onError&&t.src&&typeof t.src==`string`)return!0}return!1}function Jm(e,t){return e===`img`&&t.src!=null&&t.src!==``&&t.onLoad==null&&t.loading!==`lazy`}function Ym(e){return!(e.type===`stylesheet`&&!(e.state.loading&3))}function Xm(e){return(e.width||100)*(e.height||100)*(typeof devicePixelRatio==`number`?devicePixelRatio:1)*.25}function Zm(e,t){typeof t.decode==`function`&&(e.imgCount++,t.complete||(e.imgBytes+=Xm(t),e.suspenseyImages.push(t)),e=rh.bind(e),t.decode().then(e,e))}function Qm(e,t,n,r){if(n.type===`stylesheet`&&(typeof r.media!=`string`||!1!==matchMedia(r.media).matches)&&!(n.state.loading&4)){if(n.instance===null){var i=Pm(r.href),a=t.querySelector(Fm(i));if(a){t=a._p,typeof t==`object`&&t&&typeof t.then==`function`&&(e.count++,e=nh.bind(e),t.then(e,e)),n.state.loading|=4,n.instance=a,jt(a);return}a=t.ownerDocument||t,r=Im(r),(i=vm.get(i))&&Hm(r,i),a=a.createElement(`link`),jt(a);var o=a;o._p=new Promise(function(e,t){o.onload=e,o.onerror=t}),np(a,`link`,r),n.instance=a}e.stylesheets===null&&(e.stylesheets=new Map),e.stylesheets.set(n,t),(t=n.state.preload)&&!(n.state.loading&3)&&(e.count++,n=nh.bind(e),t.addEventListener(`load`,n),t.addEventListener(`error`,n))}}var $m=0;function eh(e,t){return e.stylesheets&&e.count===0&&ah(e,e.stylesheets),0$m?50:800)+t);return e.unsuspend=n,function(){e.unsuspend=null,clearTimeout(r),clearTimeout(i)}}:null}function th(e){if(e.count===0&&(e.imgCount===0||!e.waitingForImages)){if(e.stylesheets)ah(e,e.stylesheets);else if(e.unsuspend){var t=e.unsuspend;e.unsuspend=null,t()}}}function nh(){this.count--,th(this)}function rh(){this.imgCount--,th(this)}var ih=null;function ah(e,t){e.stylesheets=null,e.unsuspend!==null&&(e.count++,ih=new Map,t.forEach(oh,e),ih=null,nh.call(e))}function oh(e,t){if(!(t.state.loading&4)){var n=ih.get(e);if(n)var r=n.get(null);else{n=new Map,ih.set(e,n);for(var i=e.querySelectorAll(`link[data-precedence],style[data-precedence]`),a=0;a{function n(){if(typeof __REACT_DEVTOOLS_GLOBAL_HOOK__<`u`&&typeof __REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE==`function`)try{__REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE(n)}catch(e){console.error(e)}}n(),t.exports=s()}))(),l=n(),u=``;function d(e){u=e}var f=class extends Error{code;status;result;constructor(e,t,n,r){super(t),this.code=e,this.status=n,this.result=r}};async function p(e,t){let n=await fetch(`/admin/api${e}`,{...t,credentials:`same-origin`,headers:{"Content-Type":`application/json; charset=utf-8`,...u?{"X-Iris-CSRF":u}:{},...t?.headers}});if(!n.ok){let e=await n.json().catch(()=>({})),t=e.error||{};throw[`login_required`,`setup_required`,`csrf_failed`].includes(t.code)&&dispatchEvent(new Event(`iris-auth`)),new f(t.code||`request_failed`,t.message||(t.fields?`请检查输入:${t.fields.map(e=>`${e.field.split(`.`).pop()}:${e.message}`).join(`;`)}`:`请求失败,请重试`),n.status,e.result)}return n}async function m(e,t){let n=await p(e,t);return{data:await n.json(),status:n.status,headers:n.headers}}async function h(e,t){let n=await p(e,t);if(n.headers.get(`Content-Type`)?.split(`;`)[0].trim()!==`application/zip`)throw new f(`invalid_download`,`没有收到 ZIP 备份,请刷新登录状态后重试`,n.status);return{blob:await n.blob(),headers:n.headers}}async function g(e,t){return(await m(e,t)).data}var _=(e,t)=>({method:e,...t===void 0?{}:{body:JSON.stringify(t)}}),v=e=>e instanceof f?e.status===410&&[`entry_profile_legacy_write_retired`,`entry_profile_publish_mode_retired`].includes(e.code)?`旧画像的整版编辑、回滚和候选审批已退役。请在入口画像页逐条管理画像记忆;旧版本仅供查看。`:e.message:`无法连接服务,请检查服务是否仍在运行`;function y(e,t=0){let[n,r]=(0,l.useState)(null),[i,a]=(0,l.useState)(``),[o,s]=(0,l.useState)(e!==null),[c,u]=(0,l.useState)(0),d=(0,l.useCallback)(()=>{s(!0),u(e=>e+1)},[]);return(0,l.useEffect)(()=>{r(null),a(``)},[e]),(0,l.useEffect)(()=>{if(s(e!==null),e===null)return;let n=new AbortController,i=!0,o,c=async()=>{try{let t=await g(e,{signal:n.signal});i&&(r(t),a(``))}catch(e){i&&!n.signal.aborted&&a(v(e))}finally{i&&s(!1)}i&&t&&(o=setTimeout(c,t))};return c(),()=>{i=!1,n.abort(),clearTimeout(o)}},[e,t,c]),{data:n,error:i,refresh:d,loading:o}}var b=e((e=>{var t=Symbol.for(`react.transitional.element`),n=Symbol.for(`react.fragment`);function r(e,n,r){var i=null;if(r!==void 0&&(i=``+r),n.key!==void 0&&(i=``+n.key),`key`in n)for(var a in r={},n)a!==`key`&&(r[a]=n[a]);else r=n;return n=r.ref,{$$typeof:t,type:e,key:i,ref:n===void 0?null:n,props:r}}e.Fragment=n,e.jsx=r,e.jsxs=r})),x=e(((e,t)=>{t.exports=b()}))(),S=e=>e?new Date(e).toLocaleString(`zh-CN`,{month:`numeric`,day:`numeric`,hour:`2-digit`,minute:`2-digit`}):`暂无记录`,C=e=>e==null?`—`:`${(e/1e3).toFixed(1)} 秒`,w=e=>e?new Date(e).toLocaleString(`zh-CN`):`暂无记录`;function T({total:e,offset:t,limit:n=30,change:r}){return e<=n&&t===0?null:(0,x.jsxs)(`div`,{className:`pagination`,children:[(0,x.jsx)(`button`,{className:`secondary`,disabled:t===0,onClick:()=>r(Math.max(0,t-n)),children:`上一页`}),(0,x.jsxs)(`span`,{children:[`第 `,Math.floor(t/n)+1,` 页 · 共 `,e,` 条`]}),(0,x.jsx)(`button`,{className:`secondary`,disabled:t+n>=e,onClick:()=>r(t+n),children:`下一页`})]})}var E=e=>({active:`有效`,forgotten:`已遗忘`,deleted:`已删除`})[e]||e,D=e=>({normal:`正常`,temporarily_unavailable:`暂时不可用`,invalid_key:`密钥无效`,configuration_error:`配置错误`,account_problem:`账户异常`,usage_limit:`达到今日用量上限`,rate_limited:`限流退避中`})[e]||e,O=e=>({pending:`已接收 · 待学习`,batched:`已接收 · 批次处理中`,learned:`学习已完成`,abandoned:`学习放弃 · 未记住`,refused:`内容拒绝 · 未记住`,filtered:`已过滤 · 不参与学习`})[e]||e;function k(e){return e?e.state===`succeeded`?e.result?.created?.length||e.result?.updated?.length?`学习成功 · 新增 ${e.result.created?.length||0} / 更新 ${e.result.updated?.length||0}`:`学习成功,未形成新记忆`:{running:`正在学习`,waiting:e.next_retry_at?`失败后等待重试`:`等待学习`,abandoned:`已放弃 · 未记住`,refused:`已拒绝 · 未记住`}[e.state]||e.state:`暂无批次`}function A({children:e,error:t=!1}){return(0,x.jsx)(`div`,{className:`notice ${t?`error`:``}`,role:t?`alert`:`status`,children:e})}function j({title:e,children:t}){return(0,x.jsxs)(`div`,{className:`empty`,children:[(0,x.jsx)(`span`,{className:`empty-mark`,"aria-hidden":!0,children:`✳`}),(0,x.jsx)(`h3`,{children:e}),t&&(0,x.jsx)(`p`,{children:t})]})}function M({children:e,tone:t=``}){return(0,x.jsx)(`span`,{className:`badge ${t}`,children:e})}function ee({title:e,onClose:t,children:n,closeDisabled:r=!1}){let i=(0,l.useRef)(null),a=(0,l.useRef)(t);return a.current=r?()=>{}:t,(0,l.useEffect)(()=>{let e=document.activeElement;(i.current?.querySelector(`button,input,textarea,select`))?.focus();let t=e=>{if(e.key===`Escape`&&(e.preventDefault(),a.current()),e.key===`Tab`){let t=[...i.current?.querySelectorAll(`button:not(:disabled),a[href],input:not(:disabled),textarea:not(:disabled),select:not(:disabled),[tabindex="0"]`)||[]],n=t[0],r=t[t.length-1];e.shiftKey&&document.activeElement===n?(e.preventDefault(),r?.focus()):!e.shiftKey&&document.activeElement===r&&(e.preventDefault(),n?.focus())}},n=document.body.style.overflow;return document.body.style.overflow=`hidden`,document.addEventListener(`keydown`,t),()=>{document.body.style.overflow=n,document.removeEventListener(`keydown`,t),e?.focus()}},[]),(0,x.jsx)(`div`,{className:`overlay`,children:(0,x.jsxs)(`section`,{ref:i,className:`dialog`,role:`dialog`,"aria-modal":`true`,"aria-label":e,children:[(0,x.jsxs)(`div`,{className:`dialog-title`,children:[(0,x.jsx)(`h2`,{children:e}),(0,x.jsx)(`button`,{className:`icon-button`,"aria-label":`关闭详情`,disabled:r,onClick:t,children:`×`})]}),n]})})}function N({value:e}){return e?(0,x.jsx)(`time`,{dateTime:e,children:e.replace(`T`,` `).replace(/\.\d+(?=[+-]|Z$)/,``).replace(/([+-]\d{2}:\d{2}|Z)$/,` $1`)}):(0,x.jsx)(x.Fragment,{children:`暂无记录`})}function P(e,t){return Object.fromEntries(Object.keys(t).filter(n=>!Object.is(e[n],t[n])).map(e=>[e,t[e]]))}function F(e){let[t,n]=(0,l.useState)({});function r(t,r){n(n=>{let i={...n};return Object.is(e[t],r)?delete i[t]:i[t]=r,i})}return{draft:{...e,...t},change:r,reset:()=>n({})}}function I({value:e,busy:t,saveError:n,nativeImagesAvailable:r,chatEnabled:i,external:a,save:o}){let s={daily_token_limit:e.daily_token_limit,learning_concurrency:e.learning_concurrency,native_image_learning:e.native_image_learning===!0,media_retention_days:e.media_retention_days??7},{draft:c,change:u,reset:d}=F({daily_token_limit:s.daily_token_limit?.toString()||``,learning_concurrency:String(s.learning_concurrency),native_image_learning:s.native_image_learning,media_retention_days:String(s.media_retention_days)}),[f,p]=(0,l.useState)(``),[m,h]=(0,l.useState)(!1);async function g(e){if(e.preventDefault(),t)return;p(``),h(!1);let n=c.daily_token_limit.trim()?Number(c.daily_token_limit):null,r=Number(c.learning_concurrency),i=Number(c.media_retention_days);if(n!==null&&(!Number.isInteger(n)||n<1)){p(`每日 token 上限须为正整数,留空表示不限`);return}if(!c.learning_concurrency.trim()||!Number.isInteger(r)||r<1||r>32){p(`学习并发须为 1—32 的整数`);return}if(!c.media_retention_days.trim()||!Number.isInteger(i)||i<1||i>36500){p(`原图保留天数须为 1—36500 的整数`);return}let a=P(s,{daily_token_limit:n,learning_concurrency:r,native_image_learning:c.native_image_learning,media_retention_days:i});Object.keys(a).length&&await o(a)&&(d(),h(!0))}return(0,x.jsxs)(`section`,{className:`panel settings-panel`,"aria-label":`用量与学习`,children:[(0,x.jsx)(`h2`,{children:`用量与学习`}),(0,x.jsx)(`form`,{onSubmit:g,noValidate:!0,onChange:()=>h(!1),children:(0,x.jsxs)(`fieldset`,{className:`lifecycle-fieldset`,disabled:t,children:[(0,x.jsxs)(`label`,{children:[`每日 token 上限`,(0,x.jsx)(`input`,{"aria-label":`每日 token 上限`,type:`number`,min:1,step:1,value:c.daily_token_limit,placeholder:`不限`,onChange:e=>u(`daily_token_limit`,e.target.value)})]}),(0,x.jsxs)(`label`,{children:[`学习并发`,(0,x.jsx)(`input`,{"aria-label":`学习并发`,type:`number`,required:!0,min:1,max:32,step:1,value:c.learning_concurrency,onChange:e=>u(`learning_concurrency`,e.target.value)})]}),(0,x.jsx)(`p`,{className:`muted`,children:`按角色时区每日恢复;在途调用和未报告的 token 可能使实际用量超过上限。`}),(0,x.jsxs)(`label`,{children:[`原图保留天数`,(0,x.jsx)(`input`,{"aria-label":`原图保留天数`,type:`number`,required:!0,min:1,max:36500,step:1,value:c.media_retention_days,onChange:e=>u(`media_retention_days`,e.target.value)}),(0,x.jsx)(`small`,{children:`1—36500 的整数,默认 7 天;与消息保留天数分别设置。`})]}),(0,x.jsx)(`p`,{className:`lifecycle-help`,children:`到期由每日维护只清理原图字节,理解文本保留。作为记忆来源的图片、正在理解的图片受保护。`}),(0,x.jsxs)(`label`,{className:`check-label`,children:[(0,x.jsx)(`input`,{type:`checkbox`,"aria-describedby":`native-image-learning-help native-image-learning-availability`,checked:c.native_image_learning,disabled:t||!r,onChange:e=>u(`native_image_learning`,e.target.checked)}),`学习时直接看原图`]}),(0,x.jsx)(`p`,{className:`lifecycle-help`,id:`native-image-learning-help`,children:`默认关闭。开启后,后续学习把原图交给对话模型;图片描述仍用于回复准备、检索与管理页。超限或不可用的图片沿用描述或占位。`}),(0,x.jsx)(`p`,{className:`lifecycle-help`,id:`native-image-learning-availability`,children:r?a?`这是运行设置,使用外部模型配置时也可以保存。`:`使用已保存的对话模型图片能力;保存后对后续学习生效。`:`${i?`当前对话模型未启用图片输入(vision),此设置暂不生效。`:`当前对话模型未启用,无法直接看原图。`}${a?`请在外部模型配置中启用对话模型及 vision。`:`请先启用并保存对话模型的“支持图片输入(vision)”。`}`}),(f||n)&&(0,x.jsx)(A,{error:!0,children:f||n}),m&&(0,x.jsx)(A,{children:`用量与学习设置已保存`}),(0,x.jsx)(`button`,{className:`primary`,children:`保存用量与学习`})]})})]})}function te({value:e,busy:t,saveError:n,save:r}){let i={...e,internal_question_expiry_days:e.internal_question_expiry_days??7},{draft:a,change:o,reset:s}=F({default_reminder_minutes:String(i.default_reminder_minutes),overdue_reminders:i.overdue_reminders,internal_question_expiry_days:String(i.internal_question_expiry_days)}),[c,u]=(0,l.useState)(``),[d,f]=(0,l.useState)(!1);async function p(e){if(e.preventDefault(),t)return;u(``),f(!1);let n=Number(a.default_reminder_minutes),o=Number(a.internal_question_expiry_days);if(!a.default_reminder_minutes.trim()||!Number.isInteger(n)||n<0||n>525600){u(`默认提醒提前量须为 0—525600 的整数`);return}if(!a.internal_question_expiry_days.trim()||!Number.isInteger(o)||o<1||o>365){u(`学习产生的询问保留天数须为 1—365 的整数`);return}let c=P(i,{default_reminder_minutes:n,overdue_reminders:a.overdue_reminders,internal_question_expiry_days:o});Object.keys(c).length&&await r(c)&&(s(),f(!0))}return(0,x.jsxs)(`section`,{className:`panel settings-panel`,"aria-label":`目标与提醒`,children:[(0,x.jsx)(`h2`,{children:`目标与提醒`}),(0,x.jsx)(`p`,{children:`未单独填写提前量的普通目标使用默认值。询问不安排提醒,过期不会自动放弃目标。`}),(0,x.jsx)(`form`,{onSubmit:p,noValidate:!0,onChange:()=>f(!1),children:(0,x.jsxs)(`fieldset`,{className:`lifecycle-fieldset`,disabled:t,children:[(0,x.jsxs)(`label`,{children:[`默认提醒提前量(分钟)`,(0,x.jsx)(`input`,{"aria-label":`默认提醒提前量(分钟)`,type:`number`,min:0,max:525600,step:1,required:!0,value:a.default_reminder_minutes,onChange:e=>o(`default_reminder_minutes`,e.target.value)}),(0,x.jsx)(`small`,{children:`0—525600 的整数,默认 60 分钟;0 表示到期时提醒。`})]}),(0,x.jsxs)(`label`,{className:`check-label`,children:[(0,x.jsx)(`input`,{type:`checkbox`,checked:a.overdue_reminders,onChange:e=>o(`overdue_reminders`,e.target.checked)}),`启用过期提醒`]}),(0,x.jsxs)(`p`,{className:`lifecycle-help`,children:[`过期提醒每日最多一次。`,(0,x.jsx)(`a`,{href:`#/state?tab=notifications`,children:`查看提醒`}),`;“已取走”只表示宿主拿到了。`]}),(0,x.jsxs)(`label`,{children:[`学习产生的询问保留天数`,(0,x.jsx)(`input`,{"aria-label":`学习产生的询问保留天数`,type:`number`,min:1,max:365,step:1,required:!0,value:a.internal_question_expiry_days,onChange:e=>o(`internal_question_expiry_days`,e.target.value)}),(0,x.jsx)(`small`,{children:`1—365 的整数,默认 7 天。`})]}),(0,x.jsx)(`p`,{className:`lifecycle-help`,children:`只作用于学习产生且仍未结束的询问,到期由每日维护自动结束。宿主或管理员创建的询问和普通目标不受影响。`}),(c||n)&&(0,x.jsx)(A,{error:!0,children:c||n}),d&&(0,x.jsx)(A,{children:`目标与提醒设置已保存`}),(0,x.jsx)(`button`,{className:`primary`,children:`保存目标与提醒设置`})]})})]})}var L={initial_setting:`初始设定`,periodic:`定期更新`,regenerate:`重新生成`,admin_edit:`管理员编辑`,rollback:`回滚`},ne={current:`当前`,pending:`待确认`,rejected:`已拒绝`,superseded:`已被后续候选替代`,history:`历史已发布`},R={small:`小`,medium:`中`,large:`大`},re={memory:`记忆依据`,admin:`手写`,initial_template:`初始模板`},ie={modified:`记忆已修改`,forgotten:`记忆已遗忘`,deleted:`记忆已删除或清除`,no_longer_self_memory:`已不属于自我记忆`,sources_changed:`来源已变化`,daily_token_limit:`达到每日用量上限`,no_self_evidence:`暂无有效自我记忆依据`,not_due:`尚未满足定期更新条件`,evidence_changed:`依据已变化`,current_version_changed:`当前版本已变化`,interrupted:`服务中断,任务未完成`,worker_unavailable:`生成工作线程不可用`,unconfigured:`尚未配置生成模型`,unexpected_error:`生成时发生异常`,timeout:`模型调用超时`,invalid_output:`模型输出不符合要求`,paused:`模型用途已暂停`,usage_limit:`达到每日用量上限`,temporarily_unavailable:`模型暂时不可用`,configuration_error:`模型配置错误`,invalid_key:`模型密钥无效`,account_problem:`模型账户异常`,rate_limited:`模型限流`,"administrator rejected":`管理员拒绝采用`,"sentences must be an array":`句子列表格式无效`,"empty persona":`persona 正文为空`,"persona exceeds 800 characters":`persona 正文超过 800 字`,"residual memory or participant reference":`正文残留记忆或参与者编号`,"under 300 characters: concise evidence-bound text is allowed":`不足 300 字:允许依据有限时保持简短`,"initial template retains the pre-M3 length behavior":`初始模板沿用此前的长度规则`,"nonempty text required":`句子不能为空`,"one sentence per item required":`每项须为一句话`,"invalid administrator attribution":`手写归属无效`,"missing or unknown evidence":`缺少依据或依据未知`,"invalid model verdict":`模型判断格式无效`,"single-date/unknown-date evidence needs a scene qualifier":`单日或日期未知的依据需要场景限定`,"invalid change degree":`变化程度无效`,"model check must cover every sentence, in order":`模型检查须按顺序覆盖每句话`,"model check reason required":`模型检查缺少理由`};function ae(e){let t=e.match(/^sentence (\d+):\s*(.*)$/);return t?`第 ${t[1]} 句:${ae(t[2])}`:ie[e]||e}var oe=e=>e===`queued`||e===`running`;function se(e,t){return oe(e)?{queued:`等待生成`,generating:`正在生成候选`,checking:`正在检查候选`}[t]||`正在生成候选`:{current:`已发布 persona`,pending:`候选待确认,当前版本仍保留`,rejected:`候选被拒绝,当前版本仍保留`,skipped:`本次未生成候选`,conflict:`生成期间版本或依据发生变化,未发布`,failed:`生成失败,当前版本仍保留`}[e]||e}var ce={disputed:`争议`,insufficient_support:`依据不足`,superseded:`已被替代`},le={pending:`待确认`,confirmed:`已确认`,cleared:`已清除`},ue=[[`max_calls`,`记忆整理(合并/矛盾/依赖)每轮调用上限`,300],[`entry_profile_max_calls`,`入口画像每轮调用上限`,60],[`person_summary_max_calls`,`人物摘要每轮调用上限`,40],[`persona_max_calls`,`persona 每轮调用上限`,30]],de=[[`report_only_v1`,`只报告:不改动记忆,由管理员决定`],[`auto_v1`,`自主处理(默认):模型判断,程序按规则执行,事后可撤销`]],z={supersedes:`本人已更正,旧记忆进入遗忘`,reported_vs_self:`本人说法与转述冲突,转述降低一档`,coexist:`历史与当前并存,不作处理`,undecided:`无法判定,两条都保留并标注争议`},B={forget:`没有独立依据,进入遗忘`,keep:`仍有独立依据,不作改动`,undecided:`复核依据未通过校验,本次不处理`},V={memory:`记忆整理(合并/矛盾/依赖)`,entry_profile:`入口画像`,person_summary:`人物摘要`,persona:`persona`},fe=[[`merge_enabled`,`合并重复记忆`],[`conflict_enabled`,`矛盾检查与建议`],[`dependency_enabled`,`派生依据复核与建议`],[`persona_enabled`,`定期更新 persona`],[`person_summary_enabled`,`定期更新人物摘要`],[`goal_review_enabled`,`目标依据复核`]];function pe(e){return{disabled:`本次设置已关闭`,protected:`人工编辑或置顶的记忆,未自动处理`,superseded:`已被替代的记忆,等管理员恢复后才恢复有效`,superseded_member:`含已被替代且遗忘的记忆,不合并也不建议`,missing_unique_detail:`合并正文缺少某条独有细节`,numbers_changed:`合并正文的数字与原文不一致`,negation_changed:`合并正文的否定与原文不一致`,qualifiers_changed:`合并正文的限定词与原文不一致`,subject_mentions_changed:`合并正文的主体引用与原文不一致`,time_expressions_changed:`合并正文的时间表达与原文不一致`,entity_lost:`合并正文丢失了人名或实体`,entity_added:`合并正文新增了人名或实体`,new_content_word:`合并正文出现了原文没有的词`,content_word_lost:`合并正文缺少原文的词`,attribution_differs:`两条的说话人、立场或涉及人不同`,event_time_differs:`两条的事件时间不同`,visibility_differs:`两条的可见范围不同`,merge_not_allowed:`两条不符合合并条件`,entry_profile_kind:`入口画像不做正文合并`,invalid_reason:`模型理由未通过校验`,model_items_disabled:`记忆模型整理项目均已关闭`,usage_limit:`每日 token 上限已用尽`,paused:`模型用途已暂停`,interrupted:`运行已中断`,call_budget:`模型调用次数预算已用尽`,persona_busy:`已有 persona 任务正在运行`,checks_rejected:`persona 检查未通过,候选被拒绝`,revision_conflict:`对象修订已变化,跳过本次写入`,unsafe_write:`未通过写入保护校验`,invalid_response:`模型返回格式无效`,invalid_json:`模型返回的 JSON 无效`,temporary_unavailable:`模型暂时不可用`,cooldown:`尚未到定期更新时间`,no_changes:`没有需要更新的变化`,stale:`判断期间目标或依据已变化,等待重新复核`,lease_expired:`本次判断任务已超时,等待重新复核`,configuration_changed:`判断设置已变化,等待重新复核`,goal_closed:`目标已结束,取消待复核任务`,in_progress:`已有复核任务正在运行`,queue_full:`判断排队已满`,configuration:`模型尚未配置或配置无效`,budget:`预算已耗尽`,invalid:`模型输出未通过校验`,source_unavailable:`来源已不可用`,insufficient_new_memories:`新增自我记忆不足,尚未到更新条件`,too_soon:`距离上次更新尚不足一个周期`}[e]||ae(e)}var me=e=>({...e,person_summary_enabled:e.person_summary_enabled??!0,resolution:e.resolution??`report_only_v1`,entry_profile_max_calls:e.entry_profile_max_calls??60,person_summary_max_calls:e.person_summary_max_calls??40,persona_max_calls:e.persona_max_calls??30});function he({value:e,timezone:t,busy:n,saveError:r,save:i}){let a=me(e),{draft:o,change:s,reset:c}=F({...a,max_calls:String(a.max_calls),entry_profile_max_calls:String(a.entry_profile_max_calls),person_summary_max_calls:String(a.person_summary_max_calls),persona_max_calls:String(a.persona_max_calls)}),[u,d]=(0,l.useState)(``),[f,p]=(0,l.useState)(!1);async function m(e){if(e.preventDefault(),n)return;if(d(``),p(!1),!/^(?:[01][0-9]|2[0-3]):[0-5][0-9]$/.test(o.maintenance_time)){d(`整理运行时间须为 HH:MM,范围 00:00—23:59`);return}for(let[e,t]of ue){let n=Number(o[e]);if(!o[e].trim()||!Number.isInteger(n)||n<0||n>1e3){d(`${t}须为 0—1000 的整数`);return}}let t=P(a,{...o,max_calls:Number(o.max_calls),entry_profile_max_calls:Number(o.entry_profile_max_calls),person_summary_max_calls:Number(o.person_summary_max_calls),persona_max_calls:Number(o.persona_max_calls)});Object.keys(t).length&&await i(t)&&(c(),p(!0))}return(0,x.jsxs)(`section`,{className:`panel settings-panel`,"aria-label":`梦境整理设置`,id:`consolidation-settings`,children:[(0,x.jsx)(`h2`,{children:`梦境整理`}),(0,x.jsx)(`p`,{className:`lifecycle-help`,children:`整理不中断接收和学习。修改只影响之后接受的新运行,已接受的运行与续跑沿用其设置快照。`}),(0,x.jsx)(`form`,{onSubmit:m,noValidate:!0,onChange:()=>p(!1),children:(0,x.jsxs)(`fieldset`,{disabled:n,className:`lifecycle-fieldset`,children:[(0,x.jsxs)(`label`,{children:[`整理运行时间`,(0,x.jsx)(`input`,{"aria-label":`整理运行时间`,type:`text`,required:!0,maxLength:5,placeholder:`03:00`,value:o.maintenance_time,onChange:e=>s(`maintenance_time`,e.target.value)}),(0,x.jsxs)(`small`,{children:[`HH:MM · 角色时区 `,t||`未设置`,`,与每日记忆维护共用时间。`]})]}),(0,x.jsx)(`div`,{className:`form-grid`,children:ue.map(([e,t,n])=>(0,x.jsxs)(`label`,{children:[t,(0,x.jsx)(`input`,{"aria-label":t,type:`number`,min:0,max:1e3,step:1,required:!0,value:o[e],onChange:t=>s(e,t.target.value)}),(0,x.jsxs)(`small`,{children:[`0—1000 的整数,默认 `,n,` 次。`]})]},e))}),(0,x.jsx)(`p`,{className:`lifecycle-help`,children:`以上均为每轮调用上限,各步骤互不占用;HTTP 重试和 JSON 修正计入所属步骤。0 表示停用该步骤的模型调用,其他步骤、记忆生命周期维护与开启的目标依据复核仍会执行。`}),fe.map(([e,t])=>(0,x.jsxs)(`label`,{className:`check-label`,children:[(0,x.jsx)(`input`,{type:`checkbox`,checked:o[e],onChange:t=>s(e,t.target.checked)}),t]},e)),(0,x.jsx)(`p`,{className:`lifecycle-help`,children:`persona 与人物摘要需满足各自更新条件且在各自上限内至少剩余 2 次调用名额。人物摘要开关只影响之后的整理,不关闭已发布摘要的宿主读取。`}),(0,x.jsxs)(`label`,{children:[`矛盾、依赖与互有细节合并的处理方式`,(0,x.jsx)(`select`,{"aria-label":`矛盾、依赖与互有细节合并的处理方式`,value:o.resolution,onChange:e=>s(`resolution`,e.target.value),children:de.map(([e,t])=>(0,x.jsx)(`option`,{value:e,children:t},e))})]}),(0,x.jsxs)(`p`,{className:`lifecycle-help`,children:[o.resolution===`auto_v1`?`自主处理:模型只给判断和合并草稿,分数、遗忘与标注由程序按规则写入;人工编辑和置顶的记忆不动,每项都有操作记录和撤销入口。`:`只报告:矛盾和依赖复核只产生管理员可见的模型建议,不改动记忆。`,`目标依据复核不自动完成或放弃目标。`]}),(u||r)&&(0,x.jsx)(A,{error:!0,children:u||r}),f&&(0,x.jsx)(A,{children:`梦境整理设置已保存`}),(0,x.jsxs)(`div`,{className:`actions`,children:[(0,x.jsx)(`button`,{className:`primary`,children:`保存梦境整理设置`}),(0,x.jsx)(`button`,{type:`button`,onClick:()=>{c(),d(``),p(!1)},children:`还原整理设置`})]})]})}),(0,x.jsx)(`a`,{href:`#/status`,children:`查看整理报告或手动运行`})]})}function H({value:e,busy:t,save:n}){let r=e=>({enabled:e.enabled,budget_seconds:String(e.budget_seconds),concurrency:String(e.concurrency),queue_limit:String(e.queue_limit)}),[i,a]=(0,l.useState)(()=>r(e)),o=(0,l.useRef)(!1),[s,c]=(0,l.useState)(``),[u,d]=(0,l.useState)(!1);(0,l.useEffect)(()=>{o.current||a(r(e))},[e]);function f(e,t){o.current=!0,d(!1),a(n=>({...n,[e]:t}))}async function p(e){if(e.preventDefault(),t)return;c(``),d(!1);let s=Number(i.budget_seconds),l=Number(i.concurrency),u=Number(i.queue_limit);if(!i.budget_seconds.trim()||!Number.isFinite(s)||s<=0||s>10){c(`目标去重判断预算须大于 0 且不超过 10 秒`);return}if(!i.concurrency.trim()||!Number.isInteger(l)||l<1||l>8){c(`目标去重判断并发须为 1—8 的整数`);return}if(!i.queue_limit.trim()||!Number.isInteger(u)||u<0||u>64){c(`目标去重判断排队上限须为 0—64 的整数`);return}let f=await n({enabled:i.enabled,budget_seconds:s,concurrency:l,queue_limit:u});f&&(o.current=!1,a(r(f)),d(!0))}return(0,x.jsxs)(`form`,{className:`judge-settings`,onSubmit:p,noValidate:!0,"aria-label":`目标去重判断设置`,children:[(0,x.jsx)(`h3`,{children:`目标判断开关与排队`}),(0,x.jsxs)(`fieldset`,{disabled:t,className:`lifecycle-fieldset`,children:[(0,x.jsxs)(`label`,{className:`check-label`,children:[(0,x.jsx)(`input`,{type:`checkbox`,checked:i.enabled,onChange:e=>f(`enabled`,e.target.checked)}),`启用目标去重判断`]}),(0,x.jsxs)(`label`,{children:[`目标去重方法`,(0,x.jsx)(`input`,{"aria-label":`目标去重方法`,readOnly:!0,value:i.enabled?`C2(批量判断与可能重复提示)`:`A(确定性规则)`}),(0,x.jsx)(`small`,{children:`方法由后端固定;管理接口目前不提供方法切换。`})]}),(0,x.jsx)(`p`,{className:`lifecycle-help`,children:`关闭后使用确定性规则。开启时若模型暂时不可用,先保留目标并等待复核;管理员创建的目标始终只标可能重复,不自动合并。清除独立模型配置只恢复沿用,不会关闭判断。`}),(0,x.jsxs)(`div`,{className:`form-grid`,children:[(0,x.jsxs)(`label`,{children:[`目标去重判断预算(秒)`,(0,x.jsx)(`input`,{type:`number`,"aria-label":`目标去重判断预算(秒)`,min:.01,max:10,step:`any`,required:!0,value:i.budget_seconds,onChange:e=>f(`budget_seconds`,e.target.value)}),(0,x.jsx)(`small`,{children:`大于 0 且不超过 10,含排队和模型调用。`})]}),(0,x.jsxs)(`label`,{children:[`目标去重判断并发`,(0,x.jsx)(`input`,{type:`number`,min:1,max:8,step:1,required:!0,value:i.concurrency,onChange:e=>f(`concurrency`,e.target.value)})]}),(0,x.jsxs)(`label`,{children:[`目标去重判断排队上限`,(0,x.jsx)(`input`,{type:`number`,min:0,max:64,step:1,required:!0,value:i.queue_limit,onChange:e=>f(`queue_limit`,e.target.value)})]})]}),s&&(0,x.jsx)(A,{error:!0,children:s}),u&&(0,x.jsx)(A,{children:`目标去重判断设置已保存`}),(0,x.jsxs)(`div`,{className:`actions`,children:[(0,x.jsx)(`button`,{className:`primary`,children:`保存目标去重判断设置`}),(0,x.jsx)(`button`,{type:`button`,onClick:()=>{o.current=!1,a(r(e)),c(``),d(!1)},children:`还原目标判断设置`})]})]})]})}function U(){return(0,x.jsxs)(`details`,{className:`persona-guide panel`,children:[(0,x.jsx)(`summary`,{children:`persona 从哪里来?与当前状态、目标有什么区别?`}),(0,x.jsxs)(`figure`,{children:[(0,x.jsx)(`figcaption`,{children:`经历成为自我认知,经过提炼与检查后形成 persona。`}),(0,x.jsxs)(`ol`,{className:`persona-flow`,children:[(0,x.jsx)(`li`,{children:`初始背景 + 持续经历`}),(0,x.jsxs)(`li`,{children:[`自我认知`,(0,x.jsx)(`small`,{children:`有来源的自我记忆`})]}),(0,x.jsxs)(`li`,{children:[`结合生成目标 + 监管要求`,(0,x.jsx)(`small`,{children:`由管理员设定,不从记忆中改写`})]}),(0,x.jsxs)(`li`,{children:[`梦境整理中提炼与检查`,(0,x.jsx)(`small`,{children:`逐句依据与表达检查`})]}),(0,x.jsxs)(`li`,{children:[`persona`,(0,x.jsx)(`small`,{children:`按发布方式发布或等待确认`})]})]})]}),(0,x.jsxs)(`dl`,{className:`persona-distinction`,children:[(0,x.jsxs)(`div`,{children:[(0,x.jsx)(`dt`,{children:`当前状态`}),(0,x.jsx)(`dd`,{children:`现在在做什么、心情如何,由宿主报告,可能过时。`})]}),(0,x.jsxs)(`div`,{children:[(0,x.jsx)(`dt`,{children:`目标与询问`}),(0,x.jsx)(`dd`,{children:`未来要做或要问的事,跨入口共享;提醒被取走不代表目标完成。`})]}),(0,x.jsxs)(`div`,{children:[(0,x.jsx)(`dt`,{children:`persona`}),(0,x.jsx)(`dd`,{children:`相对稳定的自我表达,有版本与依据;不是当前心情,也不是待办清单。`})]})]}),(0,x.jsx)(`p`,{children:`宿主结合当前状态、目标、persona、相关记忆和本入口近期消息,组织上下文并决定说什么、做什么。`}),(0,x.jsx)(`p`,{className:`muted`,children:`“待更新”表示当前版本有失效依据;“待确认”表示已有候选等待管理员决定。两者可以同时出现。`})]})}function ge({value:e,busy:t,save:n}){let[r,i]=(0,l.useState)(e),[a,o]=(0,l.useState)(``),s=(0,l.useRef)(!1);return(0,l.useEffect)(()=>{s.current||i(e)},[e]),(0,x.jsxs)(`section`,{className:`panel settings-panel persona-settings`,children:[(0,x.jsx)(`h2`,{children:`persona 的生成与发布`}),(0,x.jsx)(`p`,{children:`生成目标和监管要求只能由管理员修改,学习和整理不能修改它们。保存只影响之后接受的任务;已接受的任务、已有候选和当前 persona 保持原设置。`}),(0,x.jsxs)(`form`,{noValidate:!0,onSubmit:async e=>{if(e.preventDefault(),t)return;let a=r.goal.trim(),c=r.rules.trim();if(!a||[...a].length>4e3){o(`生成目标须为 1—4000 字,不能只含空白`);return}if(!c||[...c].length>16e3){o(`监管要求须为 1—16000 字,不能只含空白`);return}o(``);let l=await n({goal:a,rules:c,publish_mode:r.publish_mode});l&&(s.current=!1,i(l))},children:[(0,x.jsxs)(`label`,{children:[`persona 发布方式`,(0,x.jsxs)(`select`,{disabled:t,value:r.publish_mode,onChange:e=>{s.current=!0,i({...r,publish_mode:e.target.value})},children:[(0,x.jsx)(`option`,{value:`small_medium_auto`,children:`小或中自动发布`}),(0,x.jsx)(`option`,{value:`all_auto`,children:`全部自动`}),(0,x.jsx)(`option`,{value:`all_manual`,children:`全部人工确认(默认)`})]})]}),(0,x.jsx)(`p`,{className:`lifecycle-help`,children:`M3 的 persona 门槛尚未达到,默认采用“全部人工确认”:检查通过的候选也先由管理员确认,确认后才生效。可以改回“小或中自动发布”或“全部自动”。`}),(0,x.jsx)(`p`,{className:`lifecycle-help`,children:`所有候选都要经过检查。“小或中自动发布”会自动发布检查通过的小、中变化,大变化等待确认。`}),(0,x.jsx)(A,{children:`自动候选删去或改动手写内容时按“大变化”处理。选择“全部自动”后,检查通过的大变化也会直接发布,包括删改手写内容;不会等待确认。`}),(0,x.jsxs)(`label`,{children:[`生成目标`,(0,x.jsx)(`textarea`,{rows:4,disabled:t,value:r.goal,onChange:e=>{s.current=!0,i({...r,goal:e.target.value})}})]}),(0,x.jsxs)(`label`,{children:[`监管要求`,(0,x.jsx)(`textarea`,{rows:6,disabled:t,value:r.rules,onChange:e=>{s.current=!0,i({...r,rules:e.target.value})}})]}),a&&(0,x.jsx)(A,{error:!0,children:a}),(0,x.jsx)(`button`,{className:`primary`,disabled:t,children:`保存 persona 设置`})]}),(0,x.jsx)(`p`,{children:(0,x.jsx)(`a`,{href:`#/persona`,children:`查看 persona 的依据、候选和版本历史`})}),(0,x.jsx)(U,{})]})}var _e=[[`forget_threshold`,`遗忘阈值 F`,1,99],[`restore_threshold`,`恢复阈值 H`,2,100],[`feedback_increment`,`实际使用增幅`,0,100],[`confirmation_increment`,`再次确认增幅`,0,100],[`decay_amount`,`每次衰减幅度`,0,100],[`dependency_penalty`,`依据失效扣减`,0,100],[`auto_delete_days`,`遗忘后自动删除天数`,1,36500],[`upcoming_delete_days`,`即将删除窗口(天)`,1,36500],[`message_retention_days`,`消息保留天数`,1,36500]];function ve(e){let t=Object.fromEntries(_e.map(([t])=>[t,String(e[t])]));return{...e,...t,abandoned_retry_days:e.abandoned_retry_days.join(`,`)}}function ye(e){let t=e.split(/[,,]/).map(e=>e.trim()),n=t.map(Number);return n.length<=10&&t.every(Boolean)&&n.every(e=>Number.isInteger(e)&&e>=1&&e<=365)&&n.every((e,t)=>t===0||e>n[t-1])?n:null}function be({value:e,timezone:t,managedTime:n=!1}){let[r,i]=(0,l.useState)(()=>ve(e)),a=(0,l.useRef)(!1),[o,s]=(0,l.useState)(!1),[c,u]=(0,l.useState)(``),[d,f]=(0,l.useState)(!1);(0,l.useEffect)(()=>{a.current||i(ve(e))},[e]);function p(e,t){a.current=!0,f(!1),i(n=>({...n,[e]:t}))}async function m(e){if(e.preventDefault(),o)return;u(``),f(!1);for(let[e,t,n,i]of _e){let a=Number(r[e]);if(!r[e].trim()||!Number.isInteger(a)||ai){u(`${t}须为 ${n}—${i} 的整数`);return}}if(Number(r.restore_threshold)<=Number(r.forget_threshold)){u(`恢复阈值 H 必须大于遗忘阈值 F`);return}if(!n&&!/^(?:[01][0-9]|2[0-3]):[0-5][0-9]$/.test(r.maintenance_time)){u(`维护时间须为 HH:MM 格式,范围 00:00—23:59`);return}let t=ye(r.abandoned_retry_days);if(!t){u(`自动重试日须为严格递增的 1—365 整数,用逗号分隔,最多 10 个`);return}let{maintenance_time:c,...l}={...r,...Object.fromEntries(_e.map(([e])=>[e,Number(r[e])])),abandoned_retry_days:t};s(!0);try{let e=await g(`/settings/lifecycle`,_(`PATCH`,n?l:{...l,maintenance_time:c}));i(ve(e.lifecycle)),a.current=!1,f(!0)}catch(e){u(v(e))}finally{s(!1)}}return(0,x.jsxs)(`section`,{className:`panel settings-panel full-width`,children:[(0,x.jsx)(`h2`,{children:`记忆生命周期`}),(0,x.jsx)(`p`,{className:`lifecycle-help`,children:`有效记忆低于 F 时遗忘,遗忘记忆达到 H 时恢复;两阈值之间保持原状态。置顶不自动恢复。`}),n&&(0,x.jsxs)(`p`,{className:`lifecycle-help`,children:[`每日运行时间 `,e.maintenance_time,`(`,t||`角色时区`,`),在梦境整理设置中修改。`]}),(0,x.jsx)(`form`,{onSubmit:m,noValidate:!0,children:(0,x.jsxs)(`fieldset`,{disabled:o,className:`lifecycle-fieldset`,children:[(0,x.jsxs)(`div`,{className:`lifecycle-settings-grid`,children:[_e.map(([e,t,n,i])=>(0,x.jsxs)(`label`,{children:[t,(0,x.jsx)(`input`,{"aria-label":t,type:`number`,required:!0,min:n,max:i,step:1,value:r[e],onChange:t=>p(e,t.target.value)}),(0,x.jsxs)(`small`,{children:[n,`—`,i,`,整数`]})]},e)),!n&&(0,x.jsxs)(`label`,{children:[`维护时间`,(0,x.jsx)(`input`,{"aria-label":`维护时间`,required:!0,type:`text`,inputMode:`text`,placeholder:`03:00`,maxLength:5,value:r.maintenance_time,onChange:e=>p(`maintenance_time`,e.target.value)}),(0,x.jsxs)(`small`,{children:[`HH:MM · 角色时区 `,t||`未设置`]})]})]}),(0,x.jsxs)(`label`,{className:`check-label`,children:[(0,x.jsx)(`input`,{type:`checkbox`,checked:r.auto_delete_enabled,onChange:e=>p(`auto_delete_enabled`,e.target.checked)}),`自动删除遗忘记忆`]}),(0,x.jsxs)(`label`,{className:`check-label`,children:[(0,x.jsx)(`input`,{type:`checkbox`,checked:r.abandoned_retry_enabled,onChange:e=>p(`abandoned_retry_enabled`,e.target.checked)}),`自动重试已放弃批次`]}),(0,x.jsxs)(`label`,{children:[`自动重试日`,(0,x.jsx)(`input`,{"aria-label":`自动重试日`,type:`text`,placeholder:`1,3,7`,value:r.abandoned_retry_days,onChange:e=>p(`abandoned_retry_days`,e.target.value)}),(0,x.jsx)(`small`,{children:`放弃后第几天重试,逗号分隔`})]}),(0,x.jsx)(`p`,{className:`lifecycle-help`,children:`维护按实际执行次数衰减,遗忘和置顶记忆不衰减。依据失效时每个依据只扣一次,置顶期间暂缓扣减。已放弃的批次按上面的日期(角色时区)各自动重试一次,每次使用完整尝试额度;停机错过的日期恢复后最多补一次。用完后缺口只作记录,仍可手动重新学习,手动重新学习不占自动重试次数。内容拒绝不自动重试。`}),(0,x.jsx)(`p`,{className:`lifecycle-help`,children:`关闭自动删除后仍保留天数设置,“即将删除”列表为空。消息只有超过保留期、已结束学习且没有受保护的引用时才清理。新设置用于以后接受的维护,当前运行沿用接受时的设置。`}),c&&(0,x.jsx)(A,{error:!0,children:c}),d&&(0,x.jsx)(A,{children:`生命周期设置已保存`}),(0,x.jsxs)(`div`,{className:`actions`,children:[(0,x.jsx)(`button`,{className:`primary`,children:o?`正在保存…`:`保存生命周期设置`}),(0,x.jsx)(`button`,{type:`button`,className:`secondary`,onClick:()=>{i(ve(e)),a.current=!1,u(``),f(!1)},children:`还原为最近读取的设置`})]})]})})]})}var xe={person_summary_settings:`更新人物摘要设置`,person_summary_edit:`编辑发布人物摘要`,person_summary_confirm:`确认人物摘要候选`,person_summary_reject:`拒绝人物摘要候选`,person_summary_rollback:`回滚人物摘要`,person_summary_regenerate_requested:`请求重新生成人物摘要`,person_summary_generation_requested:`请求生成人物摘要`,person_summary_generation_published:`人物摘要生成并发布`,person_summary_generation_candidate:`人物摘要待确认`,person_summary_generation_rejected:`人物摘要检查未通过`,person_summary_generation_skipped:`跳过人物摘要生成`,person_summary_generation_interrupted:`人物摘要生成中断`,token_create:`创建宿主令牌`,token_revoke:`撤销宿主令牌`,token_limits_saved:`更新宿主令牌限流`,backup_export:`导出备份`,backup_import:`从备份导入`,consolidation_settings_saved:`更新梦境整理设置`,goal_dedup_judge_saved:`更新目标去重判断设置`,consolidation_merge:`整理合并记忆`,consolidation_conflict:`整理提出矛盾建议`,consolidation_dependency:`整理提出依赖复核建议`,consolidation_annotation_confirm:`确认整理建议`,consolidation_annotation_clear:`清除整理建议`,goal_basis_review:`目标依据复核`,goal_basis_clear:`清除目标依据标注`,persona_settings_saved:`更新 persona 设置`,persona_edit:`编辑发布 persona`,persona_confirm:`确认 persona 候选`,persona_reject:`拒绝 persona 候选`,persona_rollback:`回滚 persona`,persona_current:`发布 persona`,persona_pending:`persona 候选待确认`,persona_rejected:`persona 候选被拒绝`,persona_generation_requested:`请求生成 persona`,persona_generation_current:`persona 生成并发布`,persona_generation_pending:`persona 生成后待确认`,persona_generation_rejected:`persona 生成检查未通过`,persona_generation_skipped:`跳过 persona 生成`,persona_generation_conflict:`persona 生成遇到版本或依据冲突`,persona_generation_failed:`persona 生成失败`,persona_generation_interrupted:`persona 生成中断`,goal_dedup_judged:`目标去重判断结果`,goal_dedup_queued:`目标待去重复核`,goal_create:`创建目标`,goal_update:`更新目标`,goal_merge:`合并重复目标`,goal_duplicate_dismiss:`驳回目标重复`,settings_goals:`更新目标与提醒设置`,memory_adjust:`调整记忆`,memory_forget:`手动遗忘`,memory_bulk_forget:`批量遗忘记忆`,memory_bulk_delete:`批量删除记忆`,memory_bulk_purge:`批量彻底清除记忆`,memory_restore:`恢复记忆`,memory_purge:`彻底清除记忆`,memory_recreate:`按旧内容新建`,memory_revision:`人工修订记忆`,lifecycle_saved:`更新生命周期设置`,state_settings_saved:`更新当前状态设置`,maintenance_requested:`请求维护`,maintenance_completed:`维护完成`,batch_relearn:`重新学习批次`,batch_result:`批次结果`,learn_requested:`请求立即学习`,feedback:`使用反馈`,feedback_rejected:`使用反馈被拒绝`,learn_rejected:`立即学习请求被拒绝`,trial_media_uploaded:`上传试用图片`,trial_entry_created:`创建试用入口`,trial_speaker_created:`创建发言人`,trial_message_received:`试用消息接收`,trial_prepare:`试用回复准备`,trial_reply:`试用角色回复`,setup_password:`设置管理员密码`,setup_completed:`完成首次设置`,models_saved:`更新模型配置`,role_saved:`更新角色设置`,limits_saved:`更新用量与并发`,model_retry:`请求模型重试`,model_test:`测试模型连接`,recall_judge_saved:`更新召回判断设置`,entry_visibility:`更新入口记忆可见范围`,entry_settings_updated:`更新入口节奏与过滤`,subject_alias_added:`添加人物别名`,subject_alias_removed:`删除人物别名`,subject_link_denied:`否认人物联系`,subjects_merged:`确认并合并人物`,models_imported:`导入模型配置`,login:`管理员登录`,logout:`退出登录`},Se={person_summary:`人物摘要`,local_cli:`本机命令行`,persona:`persona 更新`,consolidation:`梦境整理`,goal_dedup:`目标去重判断`,learning:`学习`,admin:`管理员`,host:`宿主`,system:`系统`,scheduler:`后台调度`,maintenance:`维护`,local_import:`本机导入`},Ce={person_summary:`人物摘要`,host_token:`宿主令牌`,backup:`备份`,persona:`persona 版本`,persona_attempt:`persona 生成任务`,goal:`目标`,memory:`记忆`,memory_bulk:`批量记忆操作`,batch:`批次`,entry:`入口`,subject:`人物`,subject_link:`人物联系`,model:`模型`,settings:`设置`,maintenance:`维护`,recall:`召回`,request:`请求`};function we({initialQuery:e=``}){let t=new URLSearchParams(e),[n,r]=(0,l.useState)({time_from:``,time_to:``,action:``,actor:``,object_type:t.get(`object_type`)||``,object_id:t.get(`object_id`)||``}),i=e=>new URLSearchParams(Object.entries(e).filter(([,e])=>!!e)).toString(),[a,o]=(0,l.useState)(()=>i(n)),[s,c]=(0,l.useState)(0),[u,d]=(0,l.useState)(``),f=y(`/operations?${a}&limit=30&offset=${s}`),p=(e,t)=>r(n=>({...n,[e]:t}));function m(e){if(e.preventDefault(),n.time_from&&n.time_to&&n.time_from>n.time_to){d(`开始日期不能晚于结束日期`);return}d(``),c(0),o(i(n)),f.refresh()}return(0,x.jsxs)(x.Fragment,{children:[(0,x.jsx)(`div`,{className:`page-heading`,children:(0,x.jsxs)(`div`,{children:[(0,x.jsx)(`p`,{className:`eyebrow`,children:`每次变更,均有记录`}),(0,x.jsx)(`h1`,{children:`操作记录`}),(0,x.jsx)(`p`,{children:`查看管理员、宿主与系统保存的操作摘要。`})]})}),(0,x.jsx)(`section`,{className:`panel`,children:(0,x.jsxs)(`form`,{onSubmit:m,className:`memory-filters`,children:[(0,x.jsxs)(`div`,{className:`filter-grid operation-filters`,children:[(0,x.jsxs)(`label`,{children:[`开始日期`,(0,x.jsx)(`input`,{type:`date`,value:n.time_from,onChange:e=>p(`time_from`,e.target.value)})]}),(0,x.jsxs)(`label`,{children:[`结束日期`,(0,x.jsx)(`input`,{type:`date`,value:n.time_to,onChange:e=>p(`time_to`,e.target.value)})]}),(0,x.jsxs)(`label`,{children:[`操作类型`,(0,x.jsxs)(`select`,{value:n.action,onChange:e=>p(`action`,e.target.value),children:[(0,x.jsx)(`option`,{value:``,children:`全部类型`}),Object.entries(xe).map(([e,t])=>(0,x.jsx)(`option`,{value:e,children:t},e))]})]}),(0,x.jsxs)(`label`,{children:[`操作者`,(0,x.jsxs)(`select`,{value:n.actor,onChange:e=>p(`actor`,e.target.value),children:[(0,x.jsx)(`option`,{value:``,children:`全部操作者`}),Object.entries(Se).map(([e,t])=>(0,x.jsx)(`option`,{value:e,children:t},e))]})]}),(0,x.jsxs)(`label`,{children:[`对象类型`,(0,x.jsxs)(`select`,{value:n.object_type,onChange:e=>p(`object_type`,e.target.value),children:[(0,x.jsx)(`option`,{value:``,children:`全部对象`}),Object.entries(Ce).map(([e,t])=>(0,x.jsx)(`option`,{value:e,children:t},e))]})]}),(0,x.jsxs)(`label`,{children:[`对象标识`,(0,x.jsx)(`input`,{value:n.object_id,maxLength:200,placeholder:`如记忆编号 8`,onChange:e=>p(`object_id`,e.target.value)})]})]}),(0,x.jsx)(`p`,{className:`fine-print`,children:`日期筛选按角色时区解释,结束日期包含当天;记录时间按本机时区显示。`}),(0,x.jsxs)(`div`,{className:`actions`,children:[(0,x.jsx)(`button`,{className:`primary`,children:`筛选记录`}),(0,x.jsx)(`button`,{type:`button`,className:`secondary`,onClick:f.refresh,children:`刷新记录`})]})]})}),(u||f.error)&&(0,x.jsx)(A,{error:!0,children:u||f.error}),(0,x.jsx)(`div`,{className:`list-heading`,children:(0,x.jsx)(`span`,{children:f.data?`共 ${f.data.total} 条`:`正在读取…`})}),f.data?.items.length===0&&(0,x.jsx)(j,{title:`没有符合条件的操作记录`}),(0,x.jsx)(`div`,{className:`learning-list`,children:f.data?.items.map(e=>(0,x.jsxs)(`article`,{className:`panel operation-card`,children:[(0,x.jsxs)(`div`,{className:`panel-heading`,children:[(0,x.jsx)(`h2`,{children:xe[e.action]||e.action}),(0,x.jsxs)(`span`,{className:`muted`,children:[`记录 #`,e.id]})]}),(0,x.jsxs)(`p`,{children:[w(e.created_at),` · `,Se[e.actor]||e.actor]}),e.object_type&&(0,x.jsxs)(`p`,{children:[`对象:`,Ce[e.object_type]||e.object_type,e.object_id===null?``:` · ${e.object_id}`]}),(0,x.jsxs)(`details`,{children:[(0,x.jsx)(`summary`,{children:`查看记录详情`}),(0,x.jsx)(`pre`,{className:`record-json`,children:JSON.stringify(e.details,null,2)})]})]},e.id))}),f.data&&(0,x.jsx)(T,{total:f.data.total,offset:s,change:c})]})}var Te=[`chat`,`embedding`,`recall_judge`,`goal_dedup_judge`,`image_understanding`],Ee={chat:`对话模型`,embedding:`Embedding 模型`,recall_judge:`召回判断模型`,goal_dedup_judge:`目标去重判断模型`,image_understanding:`图片理解模型`},De=()=>Intl.DateTimeFormat().resolvedOptions().timeZone||`Asia/Shanghai`,Oe=e=>e.inherited?{enabled:!1}:{enabled:e.enabled,base_url:e.base_url,model:e.model,dimensions:e.dimensions,reasoning_effort:e.reasoning_effort,...e.vision===void 0?{}:{vision:e.vision},...e.clearKey?{api_key:``}:e.key?{api_key:e.key}:{}};function ke({value:e,change:t}){return(0,x.jsxs)(`div`,{className:`form-grid`,children:[(0,x.jsxs)(`label`,{children:[`角色名`,(0,x.jsx)(`input`,{required:!0,maxLength:100,value:e.name,onChange:n=>t({...e,name:n.target.value})})]}),(0,x.jsxs)(`label`,{children:[`时区`,(0,x.jsx)(`input`,{required:!0,value:e.timezone||``,placeholder:`Asia/Shanghai`,onChange:n=>t({...e,timezone:n.target.value})})]}),(0,x.jsxs)(`label`,{className:`full-width`,children:[`背景材料(可选)`,(0,x.jsx)(`textarea`,{maxLength:16e3,rows:4,value:e.background,onChange:n=>t({...e,background:n.target.value})}),(0,x.jsx)(`small`,{children:`保存为“设定”的自我记忆,不会当作亲身经历。`})]})]})}function Ae({kind:e,value:t,change:n,settings:r}){let i=Ee[e],a=r.model_source===`external`,o=r.presets.find(e=>e.id===`ark-glm`)?.reasoning_effort,[s,c]=(0,l.useState)(``),[u,d]=(0,l.useState)(!1),f=e===`recall_judge`||e===`goal_dedup_judge`,p=e===`image_understanding`,m=(f||p)&&t.inherited,h=m&&!r.models[e].inherited,y=m?r.models.chat.enabled&&!h:t.enabled,b=async()=>{d(!0),c(``);try{let n=await g(`/settings/models/${e}/test`,_(`POST`,a||m?{}:Oe(t)));c(`${n.message} · ${n.duration_ms} ms`)}catch(e){c(v(e))}finally{d(!1)}};return(0,x.jsxs)(`div`,{className:`model-fields`,children:[f||p?(0,x.jsxs)(`label`,{className:`check-label`,children:[(0,x.jsx)(`input`,{type:`checkbox`,disabled:a,checked:p?t.enabled&&!t.inherited:!t.inherited,onChange:e=>{c(``),n({...t,inherited:!e.target.checked&&(!p||!!r.models.chat.vision),enabled:!p||e.target.checked})}}),`单独配置`,i]}):(0,x.jsxs)(`label`,{className:`check-label`,children:[(0,x.jsx)(`input`,{type:`checkbox`,disabled:a,checked:t.enabled,onChange:e=>n({...t,enabled:e.target.checked})}),`启用`,i]}),m?(0,x.jsxs)(`p`,{children:[p?`沿用已保存的对话模型接口、模型、密钥和推理档位。当前模型:`:`沿用已保存的对话模型接口、模型和密钥,推理档位 high。当前模型:`,r.models.chat.model||`未配置`,`。`]}):(0,x.jsxs)(`fieldset`,{disabled:a||!t.enabled,children:[e!==`embedding`&&e!==`image_understanding`&&(0,x.jsxs)(`label`,{children:[i,`服务商预设`,(0,x.jsxs)(`select`,{defaultValue:``,onChange:e=>{let i=r.presets.find(t=>t.id===e.target.value);i&&n({...t,base_url:i.base_url,model:i.model,reasoning_effort:i.reasoning_effort})},children:[(0,x.jsx)(`option`,{value:``,children:`自定义 OpenAI 兼容服务`}),r.presets.map(e=>(0,x.jsx)(`option`,{value:e.id,children:e.name},e.id))]})]}),(0,x.jsxs)(`label`,{children:[i,`接口地址`,(0,x.jsx)(`input`,{type:`url`,required:t.enabled,value:t.base_url,placeholder:`https://…/v1`,onChange:e=>n({...t,base_url:e.target.value})})]}),(0,x.jsxs)(`label`,{children:[i,`模型名`,(0,x.jsx)(`input`,{required:t.enabled,value:t.model,maxLength:200,onChange:e=>n({...t,model:e.target.value})})]}),(0,x.jsxs)(`label`,{children:[i,` API key`,(0,x.jsx)(`input`,{"aria-label":`${i} API key`,type:`password`,autoComplete:`new-password`,value:t.key||``,placeholder:t.key_set?`已设置;留空保留`:`未设置(本地服务可留空)`,onChange:e=>n({...t,key:e.target.value,clearKey:!1})}),(0,x.jsxs)(`small`,{children:[`密钥`,t.key_set?`已设置`:`未设置`,`,保存后不回显。`]})]}),t.key_set&&(0,x.jsxs)(`label`,{className:`check-label`,children:[(0,x.jsx)(`input`,{type:`checkbox`,checked:t.clearKey||!1,onChange:e=>n({...t,clearKey:e.target.checked,key:``})}),`移除已保存的密钥`]}),e===`chat`&&(0,x.jsxs)(`label`,{className:`check-label`,children:[(0,x.jsx)(`input`,{type:`checkbox`,checked:!!t.vision,onChange:e=>n({...t,vision:e.target.checked})}),`支持图片输入(vision)`,(0,x.jsx)(`small`,{children:`对话模型支持图片输入。开启后,图片理解未单独配置时沿用对话模型。学习时是否直接看原图,请在“用量与学习”中设置。`})]}),e===`embedding`?(0,x.jsxs)(`label`,{children:[i,`向量维度`,(0,x.jsx)(`input`,{type:`number`,min:1,max:65536,value:t.dimensions??``,onChange:e=>n({...t,dimensions:e.target.value?Number(e.target.value):null})}),(0,x.jsx)(`small`,{children:`可选;更换模型或维度后须重新标定召回。`})]}):(0,x.jsxs)(`label`,{children:[i,`推理档位`,(0,x.jsx)(`input`,{"aria-label":`${i}推理档位`,value:t.reasoning_effort||``,maxLength:50,placeholder:f?`留空使用 high`:`可选;留空不发送`,onChange:e=>n({...t,reasoning_effort:e.target.value||null})}),o&&e!==`image_understanding`&&(0,x.jsxs)(`small`,{children:[`火山方舟 GLM 预设使用 `,o,`。`]}),f&&(0,x.jsx)(`small`,{children:`判断档位留空时,后端使用 high。`})]})]}),(0,x.jsxs)(`div`,{className:`button-row`,children:[(0,x.jsx)(`button`,{type:`button`,disabled:u||!y,onClick:()=>void b(),children:u?`正在测试…`:`测试${i}连接`}),s&&(0,x.jsx)(`span`,{role:`status`,children:s})]}),h&&(0,x.jsx)(`p`,{className:`muted`,children:`保存沿用设置后可测试连接。`})]})}function je({done:e}){let t=y(`/settings`);return t.error?(0,x.jsxs)(A,{error:!0,children:[t.error,(0,x.jsx)(`button`,{onClick:t.refresh,children:`重新加载`})]}):t.data?(0,x.jsx)(Me,{settings:t.data,done:e}):(0,x.jsx)(`p`,{role:`status`,children:`正在读取设置…`})}function Me({settings:e,done:t}){let[n,r]=(0,l.useState)({...e.role,timezone:e.role.timezone||De()}),[i,a]=(0,l.useState)(e.models),[o,s]=(0,l.useState)(!1),[c,u]=(0,l.useState)(``);return(0,x.jsxs)(`form`,{onSubmit:async r=>{r.preventDefault(),s(!0),u(``);try{if(e.model_source===`local`)for(let t of[`chat`,`embedding`])(i[t].enabled||e.models[t].enabled)&&await g(`/settings/models/${t}`,_(`PUT`,Oe(i[t])));await g(`/setup/complete`,_(`POST`,n)),history.replaceState(null,``,`/#/trial`),t()}catch(e){u(v(e))}finally{s(!1)}},className:`setup-form`,children:[(0,x.jsxs)(`section`,{children:[(0,x.jsx)(`h2`,{children:`2 · 认识角色`}),(0,x.jsx)(ke,{value:n,change:e=>r({...e,timezone:e.timezone||De()})})]}),(0,x.jsxs)(`section`,{children:[(0,x.jsx)(`h2`,{children:`3 · 配置模型`}),(0,x.jsx)(`p`,{children:`对话模型用于学习和试用回复,也可以稍后再配。Embedding 可选,未配置时使用全文检索。`}),e.model_source===`external`&&(0,x.jsx)(A,{children:`配置来自外部文件,只读;请在外部文件中修改。`}),[`chat`,`embedding`].map(t=>(0,x.jsxs)(`div`,{className:`setup-model`,children:[(0,x.jsx)(`h3`,{children:t===`chat`?`对话模型`:`Embedding 模型(可选)`}),(0,x.jsx)(Ae,{kind:t,value:i[t],change:e=>a({...i,[t]:e}),settings:e})]},t))]}),!i.chat.enabled&&(0,x.jsx)(A,{children:`未配置模型,暂不学习;完成设置后仍可接收消息。`}),c&&(0,x.jsx)(A,{error:!0,children:c}),(0,x.jsx)(`button`,{className:`primary`,disabled:o,children:o?`正在保存…`:`完成设置并开始试用`})]})}function Ne(){let e=y(`/settings`,2e3);return(0,x.jsxs)(x.Fragment,{children:[(0,x.jsx)(`div`,{className:`page-heading`,children:(0,x.jsxs)(`div`,{children:[(0,x.jsx)(`span`,{className:`eyebrow`,children:`管理`}),(0,x.jsx)(`h1`,{children:`设置`}),(0,x.jsx)(`p`,{children:`保存后对新的工作生效。`})]})}),e.error&&(0,x.jsx)(A,{error:!0,children:e.error}),e.data?(0,x.jsx)(Pe,{settings:e.data}):(0,x.jsx)(`p`,{children:`正在读取设置…`})]})}function Pe({settings:e}){let[t,n]=(0,l.useState)(e);(0,l.useEffect)(()=>n(e),[e]);let[r,i]=(0,l.useState)({...t.role,timezone:t.role.timezone||De()}),[a,o]=(0,l.useState)(t.models),[s,c]=(0,l.useState)(``),[u,d]=(0,l.useState)(``),[f,p]=(0,l.useState)(``),[m,h]=(0,l.useState)(!1),[y,b]=(0,l.useState)(String(t.state.stale_after_minutes)),S=(0,l.useRef)(!1),C=(0,l.useRef)(new Set);(0,l.useEffect)(()=>{S.current||b(String(t.state.stale_after_minutes))},[t.state.stale_after_minutes]);let w=async(e,t,r)=>{if(!m){h(!0),d(``),p(``);try{let i=await g(e,_(t,r));n(i),e===`/settings/state`&&(S.current=!1,b(String(i.state.stale_after_minutes)));for(let t of Te)e===`/settings/models/${t}`&&(C.current.delete(t),o(e=>Object.fromEntries(Te.map(n=>[n,n===t||!C.current.has(n)?i.models[n]:e[n]]))));return p(`已保存`),i}catch(t){return c(e),d(v(t)),null}finally{h(!1)}}};return(0,x.jsxs)(`div`,{className:`settings-grid`,children:[u&&![`/settings/limits`,`/settings/goals`,`/settings/consolidation`].includes(s)&&(0,x.jsx)(A,{error:!0,children:u}),f&&(0,x.jsx)(A,{children:f}),(0,x.jsxs)(`section`,{className:`panel settings-panel`,children:[(0,x.jsx)(`h2`,{children:`角色`}),(0,x.jsxs)(`form`,{onSubmit:e=>{e.preventDefault(),w(`/settings/role`,`PATCH`,r)},children:[(0,x.jsx)(ke,{value:r,change:e=>i({...e,timezone:e.timezone||De()})}),(0,x.jsx)(`button`,{disabled:m,className:`primary`,children:`保存角色`})]})]}),t.model_source===`external`&&(0,x.jsx)(A,{children:`配置来自外部文件,只读;数据库模型配置和 secrets.json 不用于本次服务。`}),Te.filter(e=>t.models[e]).map(e=>(0,x.jsxs)(`section`,{className:`panel settings-panel`,"aria-label":Ee[e],children:[(0,x.jsx)(`h2`,{children:Ee[e]}),(0,x.jsxs)(`p`,{children:[`当前状态:`,e===`image_understanding`&&!t.models[e].enabled?`未启用`:D(t.health[e]?.state||`configuration_error`)]}),e===`image_understanding`&&(0,x.jsxs)(`p`,{children:[t.models[e].inherited?`当前图片理解沿用对话模型。`:t.models[e].enabled?`当前图片理解使用独立配置。`:`当前图片理解未启用。`,`未单独配置且对话模型开启 vision 时会自动沿用;取消独立配置会恢复此规则。 测试连接会发送一张固定测试图片,健康状态与用量仍按图片理解用途记录。`]}),(0,x.jsxs)(`form`,{onSubmit:t=>{t.preventDefault(),w(`/settings/models/${e}`,`PUT`,Oe(a[e]))},children:[(0,x.jsx)(Ae,{kind:e,value:a[e],change:t=>{C.current.add(e),o({...a,[e]:t})},settings:t}),t.model_source===`local`&&(0,x.jsxs)(`button`,{className:`primary`,disabled:m,children:[`保存`,Ee[e]]})]}),[`temporarily_unavailable`,`account_problem`].includes(t.health[e]?.state)&&(0,x.jsx)(`button`,{onClick:()=>void w(`/settings/models/${e}/retry`,`POST`,{}),children:`立即重试`}),e===`goal_dedup_judge`&&t.goal_dedup_judge&&(0,x.jsx)(H,{value:t.goal_dedup_judge,busy:m,save:async e=>(await w(`/settings/goal-dedup-judge`,`PATCH`,e))?.goal_dedup_judge||null}),e===`recall_judge`&&(0,x.jsx)(Fe,{value:t.recall_judge})]},e)),(0,x.jsx)(I,{value:t,busy:m,saveError:s===`/settings/limits`?u:``,nativeImagesAvailable:t.models.chat.enabled&&t.models.chat.vision===!0,chatEnabled:t.models.chat.enabled,external:t.model_source===`external`,save:e=>w(`/settings/limits`,`PATCH`,e)}),(0,x.jsxs)(`section`,{className:`panel settings-panel`,children:[(0,x.jsx)(`h2`,{children:`当前状态`}),(0,x.jsx)(`p`,{className:`lifecycle-help`,children:`宿主报告的活动与情绪供当前回复使用。超过设定分钟数未更新时,只标注“可能过时”,不会自动结束活动。`}),(0,x.jsxs)(`form`,{noValidate:!0,onSubmit:e=>{if(e.preventDefault(),m)return;let t=Number(y);if(!y.trim()||!Number.isInteger(t)||t<1||t>525600){p(``),c(`/settings/state`),d(`可能过时的分钟数须为 1—525600 的整数`);return}w(`/settings/state`,`PATCH`,{stale_after_minutes:t})},children:[(0,x.jsxs)(`label`,{children:[`可能过时的分钟数`,(0,x.jsx)(`input`,{"aria-label":`可能过时的分钟数`,type:`number`,required:!0,min:1,max:525600,step:1,disabled:m,value:y,onChange:e=>{S.current=!0,b(e.target.value),p(``)}}),(0,x.jsx)(`small`,{children:`1—525600 的整数,默认 30 分钟;严格超过此时长才标记。`})]}),(0,x.jsxs)(`p`,{className:`lifecycle-help`,children:[`保存后立即影响后续状态读取。`,(0,x.jsx)(`a`,{href:`#/state`,children:`查看当前状态`})]}),(0,x.jsx)(`button`,{className:`primary`,disabled:m,children:`保存当前状态设置`})]})]}),(0,x.jsx)(te,{value:t.goals,busy:m,saveError:s===`/settings/goals`?u:``,save:async e=>(await w(`/settings/goals`,`PATCH`,e))?.goals||null}),t.consolidation&&(0,x.jsx)(he,{value:t.consolidation,saveError:s===`/settings/consolidation`?u:``,timezone:t.role.timezone,busy:m,save:async e=>(await w(`/settings/consolidation`,`PATCH`,e))?.consolidation||null}),(0,x.jsx)(ge,{value:t.persona,busy:m,save:async e=>(await w(`/settings/persona`,`PATCH`,e))?.persona||null}),(0,x.jsxs)(`section`,{className:`panel settings-panel`,children:[(0,x.jsx)(`h2`,{children:`最近操作`}),(0,x.jsx)(`a`,{className:`text-button`,href:`#/operations`,children:`查看全部操作记录`}),(0,x.jsx)(`ul`,{className:`operations`,children:t.operations.map(e=>(0,x.jsxs)(`li`,{children:[(0,x.jsx)(`span`,{children:xe[e.action]||e.action}),(0,x.jsxs)(`small`,{children:[new Date(e.created_at).toLocaleString(`zh-CN`),` ·`,` `,Se[e.actor]||e.actor]})]},e.id))})]}),(0,x.jsx)(be,{managedTime:!!t.consolidation,value:t.lifecycle,timezone:t.role.timezone})]})}function Fe({value:e}){let[t,n]=(0,l.useState)(e.enabled),[r,i]=(0,l.useState)(String(e.concurrency)),[a,o]=(0,l.useState)(String(e.queue_limit)),[s,c]=(0,l.useState)(!1),[u,d]=(0,l.useState)(``),[f,p]=(0,l.useState)(!1);async function m(e){if(e.preventDefault(),!s){if(p(!1),!r.trim()||!Number.isInteger(Number(r))||Number(r)<1||Number(r)>8){d(`召回判断并发须为 1—8 的整数`);return}if(!a.trim()||!Number.isInteger(Number(a))||Number(a)<0||Number(a)>64){d(`召回判断排队上限须为 0—64 的整数`);return}c(!0),d(``);try{await g(`/settings/recall-judge`,_(`PATCH`,{enabled:t,concurrency:Number(r),queue_limit:Number(a)})),p(!0)}catch(e){d(v(e))}finally{c(!1)}}}return(0,x.jsxs)(`form`,{className:`judge-settings`,onSubmit:m,noValidate:!0,children:[(0,x.jsx)(`h3`,{children:`判断开关与排队`}),(0,x.jsx)(`p`,{children:`每次回复准备最多判断 8 条相关记忆,总预算 10 秒(含排队)。判断失败或暂停时保留仍有效的原召回;人物要点不参与判断。`}),u&&(0,x.jsx)(A,{error:!0,children:u}),f&&(0,x.jsx)(A,{children:`召回判断设置已保存`}),(0,x.jsxs)(`fieldset`,{className:`lifecycle-fieldset`,disabled:s,children:[(0,x.jsxs)(`label`,{className:`check-label`,children:[(0,x.jsx)(`input`,{type:`checkbox`,checked:t,onChange:e=>n(e.target.checked)}),`启用召回判断`]}),(0,x.jsx)(`p`,{className:`muted`,children:`关闭后保留基础召回。清除独立模型配置只会恢复沿用对话模型,不会关闭判断。`}),(0,x.jsxs)(`div`,{className:`form-grid`,children:[(0,x.jsxs)(`label`,{children:[`召回判断并发`,(0,x.jsx)(`input`,{type:`number`,min:1,max:8,step:1,required:!0,value:r,onChange:e=>i(e.target.value)})]}),(0,x.jsxs)(`label`,{children:[`召回判断排队上限`,(0,x.jsx)(`input`,{type:`number`,min:0,max:64,step:1,required:!0,value:a,onChange:e=>o(e.target.value)})]})]})]}),(0,x.jsx)(`button`,{className:`primary`,disabled:s,children:`保存召回判断设置`})]})}function Ie({children:e}){let[t,n]=(0,l.useState)(null),[r,i]=(0,l.useState)(``),a=(0,l.useCallback)(()=>{g(`/session`).then(e=>{d(e.csrf_token),n(e),i(``)}).catch(e=>i(v(e)))},[]);return(0,l.useEffect)(()=>(a(),addEventListener(`iris-auth`,a),()=>removeEventListener(`iris-auth`,a)),[a]),t?.configured&&t.authenticated?e(a):(0,x.jsxs)(`main`,{className:`access-page`,children:[(0,x.jsxs)(`div`,{className:`access-brand`,children:[(0,x.jsx)(`span`,{"aria-hidden":!0,children:`✳`}),` Iris `,(0,x.jsx)(`small`,{children:`相处与记忆`})]}),(0,x.jsxs)(`div`,{className:`panel access-card`,children:[(0,x.jsx)(`span`,{className:`eyebrow`,children:`仅本机管理`}),(0,x.jsx)(`h1`,{children:t?.configured?`管理员登录`:`首次设置`}),r&&(0,x.jsxs)(A,{error:!0,children:[r,(0,x.jsx)(`button`,{onClick:a,children:`重新连接`})]}),t?t.authenticated?(0,x.jsx)(je,{done:a}):(0,x.jsx)(Le,{creating:!t.admin_exists,configured:t.configured,setupCodeRequired:!!t.setup_code_required,done:a}):(0,x.jsx)(`p`,{children:`正在连接…`})]})]})}function Le({creating:e,configured:t,setupCodeRequired:n,done:r}){let[i,a]=(0,l.useState)(``),[o,s]=(0,l.useState)(``),[c,u]=(0,l.useState)(``),[d,f]=(0,l.useState)(``),[p,m]=(0,l.useState)(!1);return(0,x.jsxs)(`form`,{className:`password-form`,onSubmit:async n=>{if(n.preventDefault(),e&&i!==o){f(`两次密码不一致`);return}m(!0),f(``);try{await g(e?`/setup/password`:`/login`,_(`POST`,{password:i,...e&&c?{setup_code:c}:{}})),a(``),s(``),u(``),t&&location.pathname!==`/`&&history.replaceState(null,``,`/#/trial`),r()}catch(e){f(v(e))}finally{m(!1)}},children:[(0,x.jsxs)(`p`,{children:[e?`1 · 先设置管理员密码,保护记忆与设置。`:`请输入管理员密码。`,!e&&!t&&`登录后继续完成首次设置。`]}),(0,x.jsxs)(`label`,{children:[`管理员密码`,(0,x.jsx)(`input`,{required:!0,type:`password`,"aria-label":`管理员密码`,minLength:8,maxLength:256,autoComplete:e?`new-password`:`current-password`,value:i,onChange:e=>a(e.target.value)}),(0,x.jsx)(`small`,{children:`至少 8 个字符。`})]}),e&&(0,x.jsxs)(`label`,{children:[`确认密码`,(0,x.jsx)(`input`,{required:!0,type:`password`,minLength:8,maxLength:256,autoComplete:`new-password`,value:o,onChange:e=>s(e.target.value)})]}),e&&n&&(0,x.jsxs)(`label`,{children:[`设置码`,(0,x.jsx)(`input`,{required:!0,type:`password`,"aria-label":`设置码`,maxLength:128,autoComplete:`off`,value:c,onChange:e=>u(e.target.value)}),(0,x.jsx)(`small`,{children:`从服务启动日志取得;仅首次设置使用。`})]}),d&&(0,x.jsx)(A,{error:!0,children:d}),(0,x.jsx)(`button`,{className:`primary`,disabled:p,children:p?`正在验证…`:e?`设置密码并继续`:`登录`})]})}function Re({value:e}){return e.kind===`all`?(0,x.jsx)(`span`,{children:`全部入口`}):e.kind===`prefix`?(0,x.jsxs)(`span`,{children:[`入口前缀:`,(0,x.jsx)(`code`,{children:e.prefix})]}):(0,x.jsxs)(`span`,{children:[`入口列表:`,e.entries.map((e,t)=>(0,x.jsxs)(`span`,{children:[t>0&&`、`,(0,x.jsx)(`code`,{children:e})]},e))]})}function ze(){let e=y(`/tokens`,5e3),[t,n]=(0,l.useState)(!1),[r,i]=(0,l.useState)(null),[a,o]=(0,l.useState)(!1),[s,c]=(0,l.useState)(``),[u,d]=(0,l.useState)(``);return(0,x.jsxs)(`section`,{className:`panel access-panel`,"aria-label":`宿主令牌`,children:[(0,x.jsxs)(`div`,{className:`panel-heading`,children:[(0,x.jsx)(`h2`,{children:`宿主令牌`}),(0,x.jsx)(`button`,{className:`primary`,onClick:()=>{d(``),n(!0)},children:`创建宿主令牌`})]}),(0,x.jsx)(`p`,{children:`每个宿主使用自己的令牌。入口范围控制该宿主可以操作哪些入口,不是记忆可见范围设置。`}),e.data&&(0,x.jsxs)(`p`,{className:`muted`,children:[`每令牌限流:每秒 `,e.data.rate_limits.rate_per_second,` 次,突发上限`,` `,e.data.rate_limits.burst,` 次。`]}),u&&(0,x.jsx)(A,{children:u}),e.error&&(0,x.jsxs)(A,{error:!0,children:[e.error,(0,x.jsx)(`button`,{onClick:e.refresh,children:`重试令牌列表`})]}),!e.data&&!e.error&&(0,x.jsx)(`p`,{children:`正在读取令牌…`}),e.data?.items.length===0&&(0,x.jsx)(j,{title:`尚未创建宿主令牌`,children:`创建令牌后,将它配置到同一台电脑上的宿主程序。`}),(0,x.jsx)(`div`,{className:`access-records`,children:e.data?.items.map(e=>(0,x.jsxs)(`article`,{className:`access-record`,"aria-label":`${e.host} · ${e.id}`,children:[(0,x.jsxs)(`div`,{className:`panel-heading`,children:[(0,x.jsx)(`h3`,{children:e.host}),(0,x.jsx)(M,{tone:e.revoked_at?``:`green`,children:e.revoked_at?`已撤销`:`有效`})]}),(0,x.jsx)(`p`,{children:(0,x.jsx)(Re,{value:e.scope})}),(0,x.jsxs)(`dl`,{className:`access-facts`,children:[(0,x.jsxs)(`div`,{children:[(0,x.jsx)(`dt`,{children:`创建时间`}),(0,x.jsx)(`dd`,{children:(0,x.jsx)(N,{value:e.created_at})})]}),(0,x.jsxs)(`div`,{children:[(0,x.jsx)(`dt`,{children:`最近使用`}),(0,x.jsx)(`dd`,{children:e.last_used_at?(0,x.jsx)(N,{value:e.last_used_at}):`尚未使用`})]}),e.revoked_at&&(0,x.jsxs)(`div`,{children:[(0,x.jsx)(`dt`,{children:`撤销时间`}),(0,x.jsx)(`dd`,{children:(0,x.jsx)(N,{value:e.revoked_at})})]})]}),(0,x.jsxs)(`p`,{className:`muted`,children:[`令牌 ID:`,(0,x.jsx)(`code`,{children:e.id})]}),!e.revoked_at&&(0,x.jsxs)(`button`,{className:`danger-button`,onClick:()=>{c(``),i(e)},children:[`撤销 `,e.host,` 的令牌`]})]},e.id))}),(0,x.jsx)(`p`,{children:(0,x.jsx)(`a`,{href:`#/operations?object_type=host_token`,children:`查看令牌操作记录`})}),t&&(0,x.jsx)(Be,{close:()=>n(!1),created:e.refresh}),r&&(0,x.jsx)(ee,{title:`撤销宿主令牌`,onClose:()=>i(null),closeDisabled:a,children:(0,x.jsxs)(`div`,{className:`access-dialog`,children:[(0,x.jsxs)(`p`,{children:[`即将撤销 `,(0,x.jsx)(`strong`,{children:r.host}),` 的令牌(`,r.id,`)。后续宿主请求立即失效,无法恢复此令牌;再次接入需要创建新令牌。`]}),s&&(0,x.jsx)(A,{error:!0,children:s}),(0,x.jsxs)(`div`,{className:`button-row`,children:[(0,x.jsx)(`button`,{disabled:a,onClick:()=>i(null),children:`取消`}),(0,x.jsx)(`button`,{className:`danger-button`,disabled:a,onClick:async()=>{if(!a){o(!0),c(``);try{await g(`/tokens/${encodeURIComponent(r.id)}/revoke`,_(`POST`,{})),i(null),d(`令牌已撤销`),e.refresh()}catch(e){c(v(e))}finally{o(!1)}}},children:a?`正在撤销…`:`确认撤销`})]})]})})]})}function Be({close:e,created:t}){let n=y(`/catalog`),[r,i]=(0,l.useState)(``),[a,o]=(0,l.useState)(`entries`),[s,c]=(0,l.useState)(``),[u,d]=(0,l.useState)(``),[f,p]=(0,l.useState)(!1),[m,h]=(0,l.useState)(``),[b,S]=(0,l.useState)(null),[C,w]=(0,l.useState)(``);return(0,x.jsx)(ee,{title:b?`令牌已创建 · 仅显示这一次`:`创建宿主令牌`,onClose:e,closeDisabled:f,children:(0,x.jsx)(`div`,{className:`access-dialog`,children:b?(0,x.jsxs)(x.Fragment,{children:[(0,x.jsxs)(A,{children:[`请现在复制并交给 `,b.host,` `,`保存。关闭此窗口、离开页面或刷新后,无法再次查看令牌原文;丢失后只能撤销并重新创建。`]}),(0,x.jsxs)(`label`,{children:[`新令牌原文`,(0,x.jsx)(`textarea`,{className:`token-secret`,readOnly:!0,rows:3,spellCheck:!1,autoComplete:`off`,value:b.token,onFocus:e=>e.target.select()})]}),(0,x.jsxs)(`div`,{className:`button-row`,children:[(0,x.jsx)(`button`,{className:`primary`,onClick:async()=>{try{await navigator.clipboard.writeText(b.token),w(`已复制,请妥善保存`)}catch{w(`无法自动复制,请选择令牌原文并手动复制。`)}},children:`复制令牌`}),(0,x.jsx)(`button`,{onClick:e,children:`关闭并清除原文`})]}),C&&(0,x.jsx)(A,{children:C})]}):(0,x.jsx)(`form`,{noValidate:!0,onSubmit:async e=>{if(e.preventDefault(),f)return;let n=s.split(/\r?\n/).filter(e=>e!==``);if(!r.trim()||[...r.trim()].length>100){h(`宿主名称须为 1—100 字,不能只含空白`);return}if(a===`entries`&&(!n.length||n.length>1e3||new Set(n).size!==n.length||n.some(e=>!e.trim()||[...e].length>200||e.includes(`\0`)))){h(`请填写 1—1000 个不重复的入口 ID,每行一个,每个为 1—200 字,不能只含空白。`);return}if(a===`prefix`&&(!u.trim()||[...u].length>200||u.includes(`\0`))){h(`入口前缀须为 1—200 字,不能只含空白`);return}let i=a===`entries`?{kind:a,entries:n}:a===`prefix`?{kind:a,prefix:u}:{kind:a};p(!0),h(``);try{let e=await g(`/tokens`,_(`POST`,{host:r.trim(),scope:i}));S(e),t()}catch(e){h(`${v(e)}。如请求已提交但结果不明确,请刷新列表核对;若令牌已创建却未取得原文,请撤销后重新创建。`),t()}finally{p(!1)}},children:(0,x.jsxs)(`fieldset`,{disabled:f,children:[(0,x.jsxs)(`label`,{children:[`宿主名称`,(0,x.jsx)(`input`,{value:r,maxLength:100,onChange:e=>i(e.target.value),placeholder:`例如:本机聊天机器人`})]}),(0,x.jsxs)(`label`,{children:[`入口范围`,(0,x.jsxs)(`select`,{value:a,onChange:e=>o(e.target.value),children:[(0,x.jsx)(`option`,{value:`entries`,children:`入口列表`}),(0,x.jsx)(`option`,{value:`prefix`,children:`入口前缀`}),(0,x.jsx)(`option`,{value:`all`,children:`全部入口`})]})]}),a===`entries`&&(0,x.jsxs)(x.Fragment,{children:[!!n.data?.entries.length&&(0,x.jsxs)(`label`,{children:[`添加已有入口`,(0,x.jsxs)(`select`,{value:``,onChange:e=>{e.target.value&&!s.split(/\r?\n/).includes(e.target.value)&&c(s?`${s}\n${e.target.value}`:e.target.value)},children:[(0,x.jsx)(`option`,{value:``,children:`选择入口加入列表`}),n.data.entries.map(e=>(0,x.jsxs)(`option`,{value:e.id,children:[e.name,` · `,e.id]},e.id))]})]}),(0,x.jsxs)(`label`,{children:[`入口 ID 列表`,(0,x.jsx)(`textarea`,{rows:4,value:s,onChange:e=>c(e.target.value),placeholder:`group-a -private-a`})]}),n.error&&(0,x.jsx)(`p`,{className:`muted`,children:`已有入口暂时无法读取,仍可手动填写入口 ID。`})]}),a===`prefix`&&(0,x.jsxs)(`label`,{children:[`入口 ID 前缀`,(0,x.jsx)(`input`,{value:u,onChange:e=>d(e.target.value),maxLength:200,placeholder:`例如:astrbot:`})]}),(0,x.jsx)(`p`,{children:`列表和前缀按入口 ID 原字符匹配,区分大小写;前缀不是通配符。可以授权尚未创建的入口。范围创建后不可修改,需要调整时请新建令牌并撤销旧令牌。`}),a===`all`&&(0,x.jsx)(A,{children:`此令牌可访问全部现有及今后创建的入口。`}),m&&(0,x.jsx)(A,{error:!0,children:m}),(0,x.jsx)(`button`,{className:`primary`,disabled:f,children:f?`正在创建…`:`创建并显示令牌`})]})})})})}function Ve(e){return Number.isFinite(e)?e<1024?`${e} B`:e<1024**2?`${(e/1024).toFixed(1)} KiB`:`${(e/1024**2).toFixed(1)} MiB`:`未记录`}function He(){let e=y(`/backups?limit=30`,5e3),[t,n]=(0,l.useState)(!1),[r,i]=(0,l.useState)(!1),[a,o]=(0,l.useState)(``),[s,c]=(0,l.useState)(null),u=(0,l.useRef)(null);return(0,l.useEffect)(()=>()=>u.current?.abort(),[]),(0,l.useEffect)(()=>()=>{s&&URL.revokeObjectURL(s.url)},[s]),(0,x.jsxs)(x.Fragment,{children:[(0,x.jsxs)(`section`,{className:`panel access-panel`,"aria-label":`导出备份`,children:[(0,x.jsx)(`h2`,{children:`导出备份`}),(0,x.jsx)(`p`,{children:`服务运行时即可导出数据库与媒体的一致快照。备份包含原始消息和记忆,请妥善保存。`}),(0,x.jsxs)(`form`,{onSubmit:async n=>{if(n.preventDefault(),r)return;let a=new AbortController;u.current=a,i(!0),o(``),c(null);try{let{blob:e,headers:n}=await h(`/backups/export`,{..._(`POST`,{include_secrets:t}),signal:a.signal});if(a.signal.aborted)return;let r=n.get(`X-Iris-Backup-Includes-Secrets`)===`true`||t,i=n.get(`X-Iris-Backup-Id`),o=`iris-backup${i&&/^[a-f0-9]{32}$/.test(i)?`-${i}`:``}${r?`-with-secrets`:``}.zip`,s=URL.createObjectURL(e);c({url:s,filename:o,includesSecrets:r});let l=document.createElement(`a`);l.href=s,l.download=o,document.body.append(l),l.click(),l.remove()}catch(e){a.signal.aborted||o(v(e))}finally{a.signal.aborted||(i(!1),e.refresh())}},children:[(0,x.jsxs)(`label`,{className:`check-label`,children:[(0,x.jsx)(`input`,{type:`checkbox`,checked:t,disabled:r,onChange:e=>n(e.target.checked),"aria-describedby":`backup-key-help`}),`包含模型密钥`]}),(0,x.jsx)(`p`,{id:`backup-key-help`,className:`muted`,children:`默认不包含模型密钥。选择包含时只导出本实例已保存的密钥,不包含外部模型配置文件或环境变量中的密钥。`}),t&&(0,x.jsxs)(A,{error:!0,children:[(0,x.jsx)(`strong`,{children:`包含模型密钥的 ZIP 不加密。`}),`请只保存到可信位置,不要分享或提交到代码仓库。`]}),(0,x.jsx)(`button`,{className:`primary`,disabled:r,children:r?`正在导出,请稍候…`:t?`导出并下载(含模型密钥)`:`导出并下载备份`})]}),a&&(0,x.jsxs)(A,{error:!0,children:[a,`。如未取得备份文件,请重新导出。`]}),s&&(0,x.jsxs)(A,{children:[`已生成备份并请求浏览器下载。若下载未开始,可`,(0,x.jsx)(`a`,{href:s.url,download:s.filename,children:`保存本次备份`}),`。`,s.includesSecrets&&(0,x.jsx)(`p`,{children:(0,x.jsx)(`strong`,{children:`本次备份含未加密的模型密钥,请勿分享。`})})]}),(0,x.jsx)(`h3`,{children:`最近的导出记录`}),(0,x.jsx)(`p`,{className:`muted`,children:`最多显示最近 30 次。记录不保留可再次下载的文件,需要时请重新导出。`}),e.error&&(0,x.jsxs)(A,{error:!0,children:[e.error,(0,x.jsx)(`button`,{onClick:e.refresh,children:`重试导出记录`})]}),!e.data&&!e.error&&(0,x.jsx)(`p`,{children:`正在读取导出记录…`}),e.data?.items.length===0&&(0,x.jsx)(j,{title:`还没有导出记录`}),(0,x.jsx)(`div`,{className:`access-records`,children:e.data?.items.map(e=>(0,x.jsxs)(`article`,{className:`access-record`,children:[(0,x.jsxs)(`div`,{className:`panel-heading`,children:[(0,x.jsx)(`h4`,{children:(0,x.jsx)(N,{value:e.created_at})}),(0,x.jsx)(M,{tone:e.includes_model_secrets?`danger`:`green`,children:e.includes_model_secrets?`含模型密钥`:`不含模型密钥`})]}),(0,x.jsxs)(`p`,{children:[Ve(e.size),` · `,e.file_count??`未记录`,` 个文件 · `,e.media_included?`含媒体`:`无媒体`]}),(0,x.jsxs)(`p`,{children:[`格式版本 `,e.format_version??`未记录`,` · 迁移版本`,` `,e.migration_version??`未记录`]}),(0,x.jsxs)(`p`,{children:[(e.trigger||e.purpose)===`scheduled`?`定时`:(e.trigger||e.purpose)===`pre_import`?`导入前自动备份`:`手动导出`,` `,`·`,` `,e.actor===`admin`?`管理员`:e.actor===`local_cli`?`本机命令行`:e.actor===`scheduler`?`调度器`:e.actor]}),(0,x.jsxs)(`p`,{className:`muted`,children:[`备份 ID:`,(0,x.jsx)(`code`,{children:e.backup_id})]})]},e.backup_id))}),(0,x.jsx)(`p`,{children:(0,x.jsx)(`a`,{href:`#/operations?object_type=backup`,children:`查看导出与导入操作记录`})})]}),(0,x.jsxs)(`section`,{className:`panel access-panel`,"aria-label":`离线导入`,children:[(0,x.jsx)(`h2`,{children:`从备份导入`}),(0,x.jsx)(`p`,{children:`导入需要停止 Iris 服务,在本机命令行操作。先确认备份与目标数据目录,再按以下步骤恢复。`}),(0,x.jsxs)(`ol`,{className:`import-steps`,children:[(0,x.jsx)(`li`,{children:`停止 Iris 服务和其他配置命令。`}),(0,x.jsxs)(`li`,{children:[`将示例路径替换为自己的路径。导入到新目录:`,(0,x.jsx)(`pre`,{children:(0,x.jsx)(`code`,{children:`uv run iris --data-dir /path/to/new-data backup import /path/to/backup.zip`})})]}),(0,x.jsxs)(`li`,{children:[`覆盖已有实例时,确认目标后添加覆盖参数:`,(0,x.jsx)(`pre`,{children:(0,x.jsx)(`code`,{children:`uv run iris --data-dir /path/to/iris-data backup import /path/to/backup.zip --confirm-overwrite`})}),`覆盖前会在数据目录旁自动备份现有实例;备份失败不会覆盖。`]}),(0,x.jsx)(`li`,{children:`导入完成后按原方式启动服务,检查记忆、媒体与模型设置。`})]}),(0,x.jsx)(A,{children:`不含模型密钥的备份导入后,需要重新配置密钥,或继续使用原有外部配置;不会沿用目标目录原来的模型密钥。`}),(0,x.jsx)(`p`,{children:`覆盖前会单独保存目标实例原有的模型密钥,以便人工恢复;请私密保存该归档。它不会自动配给导入后的模型设置。`}),e.data?.import_instructions&&(0,x.jsxs)(`p`,{className:`muted`,children:[`服务提供的导入说明:`,e.data.import_instructions]})]})]})}function Ue(){let e=`http://127.0.0.1${location.port?`:${location.port}`:``}/api/v1`;return(0,x.jsxs)(x.Fragment,{children:[(0,x.jsx)(`div`,{className:`page-heading`,children:(0,x.jsxs)(`div`,{children:[(0,x.jsx)(`p`,{className:`eyebrow`,children:`接入与备份`}),(0,x.jsx)(`h1`,{children:`接入与数据`}),(0,x.jsx)(`p`,{children:`连接同一台电脑上的宿主,管理令牌与实例备份。`})]})}),(0,x.jsxs)(`section`,{className:`panel access-panel`,"aria-label":`接入说明`,children:[(0,x.jsx)(`h2`,{children:`把宿主连到 Iris`}),(0,x.jsxs)(`p`,{children:[`宿主程序与 Iris 在同一台电脑上运行。先创建令牌,在宿主配置中保存,再访问`,` `,(0,x.jsx)(`code`,{children:e}),`。端口与当前 Iris 服务一致。`]}),(0,x.jsxs)(`p`,{children:[`请求使用 `,(0,x.jsx)(`code`,{children:`Authorization: Bearer <令牌>`}),`。管理员登录会话不能代替宿主令牌;Iris 的宿主接口负责接收消息与准备回复,不生成对外回复。`]}),(0,x.jsxs)(`div`,{className:`button-row`,children:[(0,x.jsx)(`a`,{href:`/docs`,target:`_blank`,rel:`noreferrer`,children:`打开接口文档`}),(0,x.jsx)(`a`,{href:`/openapi.json`,target:`_blank`,rel:`noreferrer`,children:`查看 OpenAPI 契约`})]})]}),(0,x.jsx)(ze,{}),(0,x.jsx)(`div`,{className:`access-data-grid`,children:(0,x.jsx)(He,{})})]})}function We({value:e,openMemory:t}){return(0,x.jsxs)(`div`,{className:`trial-diagnostic`,"aria-label":`召回候选与判断分`,children:[(0,x.jsxs)(`p`,{className:`fine-print`,children:[`触发消息 #`,e.message_id,` · 准备于 `,w(e.prepared_at),(0,x.jsx)(`br`,{}),`召回记录 `,e.recall_id]}),!e.candidates.length&&(0,x.jsx)(`p`,{children:`本次没有额外召回候选。`}),e.candidates.map(e=>(0,x.jsxs)(`div`,{className:`entry-profile-evidence`,children:[(0,x.jsxs)(`button`,{className:`text-button`,onClick:()=>t(e.id),children:[`记忆 #`,e.id,` · 当时修订 `,e.revision]}),(0,x.jsxs)(`p`,{children:[e.reason===`person_highlight`?`人物要点(不参与判断)`:e.support==null?`判断分:未取得`:`判断分:${e.support} / 100`,` `,(0,x.jsx)(M,{children:{retained:`保留`,removed:`已去掉`,stale:`复核时失效`}[e.outcome]||e.outcome})]})]},e.id)),(0,x.jsx)(`p`,{className:`fine-print`,children:`以上是当次准备的诊断,链接打开记忆的当前详情;查看诊断不会再次召回或增加使用次数。`})]})}var Ge=[`image/png`,`image/jpeg`,`image/gif`,`image/webp`],Ke=e=>e>=1048576?`${(e/1048576).toFixed(1)} MiB`:`${Math.ceil(e/1024)} KiB`;function qe({media:e}){return e?.length?(0,x.jsx)(`div`,{className:`message-media`,children:e.map((e,t)=>(0,x.jsx)(Je,{item:e,number:t+1},e.id))}):null}function Je({item:e,number:t}){let[n,r]=(0,l.useState)(!1),i=e.kind===`image`&&Ge.includes(e.content_type),a=i&&e.understanding_source===`unprocessed`&&!!e.completed_at,o=i&&e.understanding_source===`unprocessed`&&!e.completed_at,s=e.kind===`audio`?`音频`:e.kind===`video`?`视频`:`文件`;return(0,x.jsxs)(`div`,{className:`media-card`,children:[(0,x.jsx)(`a`,{href:e.file_url,target:`_blank`,rel:`noopener noreferrer`,"aria-label":i?`查看图片 ${t} 原图`:`查看${s}附件 ${t}`,children:i?n?(0,x.jsx)(`span`,{children:`图片不可用或已清理`}):(0,x.jsx)(`img`,{src:e.file_url,alt:`图片 ${t}`,loading:`lazy`,onError:()=>r(!0)}):`${s}附件 ${t}`}),(0,x.jsxs)(`small`,{children:[e.content_type,` · `,Ke(e.size_bytes)]}),(0,x.jsx)(`div`,{children:(0,x.jsx)(M,{tone:a||e.understanding_source===`refused`?`danger`:``,children:e.understanding_source_label})}),a&&(0,x.jsx)(`strong`,{className:`danger-text`,children:`理解失败`}),o&&(0,x.jsxs)(x.Fragment,{children:[(0,x.jsx)(`strong`,{children:`等待理解/理解中`}),(0,x.jsx)(`small`,{children:`未启用或暂停时也会保持等待。`})]}),(0,x.jsx)(`p`,{className:`media-description`,children:e.understanding_text}),e.completed_at&&(0,x.jsxs)(`small`,{children:[`完成时间:`,w(e.completed_at)]})]})}var Ye=10485760;function Xe(e,t){return t.throwIfAborted(),new Promise((n,r)=>{let i=new FileReader,a=()=>i.abort();i.onload=()=>n(String(i.result).split(`,`,2)[1]),i.onerror=()=>r(new f(`image_read_failed`,`无法读取图片 ${e.name},请重新选择`,0)),i.onabort=()=>r(new DOMException(`Aborted`,`AbortError`)),i.onloadend=()=>t.removeEventListener(`abort`,a),t.addEventListener(`abort`,a,{once:!0}),i.readAsDataURL(e)})}function Ze(){let[e,t]=(0,l.useState)([]),n=(0,l.useRef)([]),r=(0,l.useRef)(new Map),i=e=>{n.current=e,t(e)};(0,l.useEffect)(()=>()=>{for(let e of n.current)URL.revokeObjectURL(e.url);r.current.clear()},[]);function a(e){if(n.current.length+e.length>100)throw new f(`too_many_images`,`每条消息最多发送 100 张图片`,0);for(let t of e){if(!Ge.includes(t.type))throw new f(`unsupported_media_type`,`${t.name}:仅支持 PNG、JPEG、GIF、WebP 图片`,0);if(t.size>Ye)throw new f(`media_too_large`,`${t.name}:图片超过单文件 10 MiB 上限`,413);if(!t.size)throw new f(`empty_image`,`${t.name}:图片文件为空`,0)}i([...n.current,...e.map(e=>({file:e,key:crypto.randomUUID(),url:URL.createObjectURL(e)}))])}function o(e){let t=n.current.find(t=>t.key===e);t&&URL.revokeObjectURL(t.url),r.current.delete(e),i(n.current.filter(t=>t.key!==e))}function s(){for(let e of n.current)URL.revokeObjectURL(e.url);r.current.clear(),i([])}async function c(e,t){let i=[],a=n.current;for(let[n,o]of a.entries()){e.throwIfAborted();let s=r.current.get(o.key);if(!s){t(`正在上传图片 ${n+1}/${a.length}…`);let i=await Xe(o.file,e);e.throwIfAborted();let c=await g(`/media`,{..._(`POST`,{content_type:o.file.type,data_base64:i}),signal:e});e.throwIfAborted(),s=c.id,r.current.set(o.key,s)}i.push(s)}return i}return{images:e,add:a,remove:o,clear:s,upload:c}}function Qe({images:e,busy:t,add:n,remove:r}){return(0,x.jsxs)(`div`,{className:`trial-images`,children:[(0,x.jsxs)(`label`,{children:[`选择图片`,(0,x.jsx)(`input`,{type:`file`,accept:Ge.join(`,`),multiple:!0,disabled:t,onChange:e=>{n(Array.from(e.target.files||[])),e.target.value=``}})]}),(0,x.jsx)(`p`,{className:`fine-print`,children:`也可在消息框粘贴图片。PNG/JPEG/GIF/WebP,每张最多 10 MiB,可只发送图片。`}),!!e.length&&(0,x.jsx)(`ul`,{className:`draft-images`,"aria-label":`待发送图片`,children:e.map(e=>(0,x.jsxs)(`li`,{children:[(0,x.jsx)(`img`,{src:e.url,alt:`待发送图片:${e.file.name}`}),(0,x.jsxs)(`span`,{children:[e.file.name,` · `,Ke(e.file.size)]}),(0,x.jsx)(`button`,{type:`button`,disabled:t,onClick:()=>r(e.key),"aria-label":`移除图片 ${e.file.name}`,children:`移除`})]},e.key))})]})}function $e(){let e=y(`/persona`,2e3),t=e.data;return(0,x.jsxs)(`section`,{className:`persona-summary`,"aria-label":`试用 persona`,children:[e.error&&(0,x.jsxs)(A,{error:!0,children:[e.error,`。persona 可能尚未更新。`,(0,x.jsx)(`button`,{onClick:e.refresh,children:`重试 persona`})]}),t?.needs_update&&(0,x.jsxs)(`p`,{children:[(0,x.jsx)(M,{tone:`warning`,children:`待更新`}),` `,t.stale_basis_count,` `,`条依据已失效,当前版本仍在使用。`]}),t?.pending&&(0,x.jsxs)(A,{children:[(0,x.jsx)(M,{tone:`warning`,children:`待确认`}),` `,(0,x.jsx)(`strong`,{children:`1 个 persona 候选等待确认`}),(0,x.jsxs)(`p`,{children:[`候选 v`,t.pending.id,` 尚未生效,当前版本仍在使用。`]}),(0,x.jsx)(`a`,{href:`#/persona?tab=current&focus=pending`,children:`查看候选与差异`})]}),(0,x.jsxs)(`details`,{open:!0,children:[(0,x.jsxs)(`summary`,{children:[`当前 persona`,t?.current?` · v${t.current.id}`:``]}),(0,x.jsx)(`p`,{className:`persona-content`,children:t?t.current?.content||`暂无 persona 版本`:`正在读取 persona…`})]}),(0,x.jsx)(`a`,{href:`#/persona`,children:`查看 persona 与自我`})]})}function et(e,t){let n=t.find(t=>t.id===e);return n?`${n.name}(${e})`:e}function tt(e,t){return e.visibility===`shared`?`全部入口共享(默认)`:e.visibility===`entry_only`?`仅本入口`:e.visibility===`entries`?`指定入口:${e.visible_in.map(e=>et(e,t)).join(`、`)}`:`未返回,请重新加载`}function nt(e,t){return e?e.shared?`全部入口`:e.visible_in.length?e.visible_in.map(e=>et(e,t)).join(`、`):`仅管理员可见`:`未返回,请重新加载详情`}function rt({entry:e,entries:t,value:n,busy:r,setBusy:i,onSaved:a}){let[o,s]=(0,l.useState)(n.visibility),[c,u]=(0,l.useState)(n.visible_in.filter(t=>t!==e.id)),[d,f]=(0,l.useState)(``);async function p(t){if(t.preventDefault(),!r){i(!0),f(``);try{await g(`/entries/${encodeURIComponent(e.id)}/visibility`,_(`PATCH`,{visibility:o,visible_in:o===`entries`?c:[]})),a()}catch(e){f(v(e))}finally{i(!1)}}}return(0,x.jsxs)(`form`,{className:`entry-settings-form entry-visibility-form`,onSubmit:p,children:[(0,x.jsx)(`h3`,{children:`记忆可见范围`}),(0,x.jsx)(`p`,{className:`muted`,id:`entry-visibility-help`,children:`这是查询时生效的设置。修改后立即影响该入口来源的全部已有记忆,无需重新学习,可随时改回。 多个来源或派生依据的限制共同生效,最终范围可在记忆详情查看。`}),d&&(0,x.jsx)(A,{error:!0,children:d}),(0,x.jsxs)(`fieldset`,{className:`lifecycle-fieldset`,disabled:r,children:[(0,x.jsxs)(`label`,{children:[`记忆可见范围`,(0,x.jsxs)(`select`,{value:o,"aria-describedby":`entry-visibility-help`,onChange:e=>s(e.target.value),children:[(0,x.jsx)(`option`,{value:`shared`,children:`全部入口共享(默认)`}),(0,x.jsx)(`option`,{value:`entry_only`,children:`仅本入口`}),(0,x.jsx)(`option`,{value:`entries`,children:`指定入口`})]})]}),o===`entries`&&(0,x.jsxs)(`fieldset`,{className:`lifecycle-fieldset`,children:[(0,x.jsx)(`legend`,{children:`选择可见入口(可多选)`}),(0,x.jsxs)(`label`,{className:`check-label`,children:[(0,x.jsx)(`input`,{type:`checkbox`,checked:!0,disabled:!0}),et(e.id,[e]),` · 本入口始终包含`]}),t.filter(t=>t.id!==e.id).map(e=>(0,x.jsxs)(`label`,{className:`check-label`,children:[(0,x.jsx)(`input`,{type:`checkbox`,checked:c.includes(e.id),onChange:t=>{u(t.target.checked?[...c,e.id]:c.filter(t=>t!==e.id))}}),et(e.id,t)]},e.id)),t.every(t=>t.id===e.id)&&(0,x.jsx)(`p`,{className:`muted`,children:`暂无其他入口可选。`})]}),(0,x.jsx)(`p`,{className:`fine-print`,children:`可见范围与下方的节奏、过滤分别保存。`}),(0,x.jsx)(`button`,{className:`primary`,disabled:r,children:r?`正在保存…`:`保存可见范围`})]})]})}var it={realtime:{name:`实时`,value:{count:4,idle_seconds:5,max_wait_seconds:60}},standard:{name:`标准`,value:{count:12,idle_seconds:600,max_wait_seconds:3600}},economy:{name:`省流`,value:{count:30,idle_seconds:1800,max_wait_seconds:14400}}};function at(e){if(typeof e==`string`&&it[e])return it[e].name;try{let t=typeof e==`string`?JSON.parse(e):e;return`自定义 · ${t.count} 条 / 空闲 ${t.idle_seconds} 秒 / 最长 ${t.max_wait_seconds} 秒`}catch{return`未知节奏`}}function ot({value:e}){return e?(0,x.jsxs)(`div`,{className:`entry-filter-summary`,children:[(0,x.jsxs)(`p`,{children:[`最短字数:`,e.min_chars?`${e.min_chars} 字`:`不限制`]}),(0,x.jsxs)(`p`,{children:[`提及过滤:`,e.mention_only?`只学习提到角色的消息及前后各 ${e.context_messages} 条`:`关闭`]}),(0,x.jsxs)(`p`,{children:[`每小时批次上限:`,e.max_batches_per_hour||`不限制`]})]}):null}function st({value:e}){return e?(0,x.jsxs)(`div`,{className:`entry-queue-wait`,children:[e.reason&&(0,x.jsx)(`p`,{className:`queue-reason`,children:e.reason===`hourly_batch_limit`?`达到每小时批次上限,消息排队等待`:e.reason===`filter_context`?`等待提及窗口的后续消息或空闲定案`:e.reason}),e.reason===`hourly_batch_limit`&&e.retry_at&&(0,x.jsxs)(`p`,{children:[`最早释放额度:`,w(e.retry_at),`(不是完成时间)`]}),e.batches_last_hour!==null&&(0,x.jsxs)(`p`,{className:`muted`,children:[`滚动 60 分钟已组成 `,e.batches_last_hour,` / `,e.limit,` 个批次`]}),(0,x.jsxs)(`p`,{className:`muted`,children:[`待过滤定案 `,e.filter_waiting_count,` 条 · 已过滤`,` `,e.filtered_count,` 条`]})]}):null}function ct({entry:e,entries:t,onClose:n,onSaved:r}){let i=y(`/entries/${encodeURIComponent(e.id)}/settings`),[a,o]=(0,l.useState)(!1);return(0,x.jsxs)(ee,{title:`入口设置 · ${e.name}`,onClose:n,closeDisabled:a,children:[i.error&&(0,x.jsxs)(A,{error:!0,children:[i.error,(0,x.jsx)(`button`,{onClick:i.refresh,children:`重新加载`})]}),i.data?(0,x.jsxs)(x.Fragment,{children:[i.data.visibility?(0,x.jsx)(rt,{entry:e,entries:t,value:i.data,busy:a,setBusy:o,onSaved:r}):(0,x.jsxs)(A,{error:!0,children:[`未读到可见范围,请重新加载入口设置。`,(0,x.jsx)(`button`,{onClick:i.refresh,children:`重新加载`})]}),(0,x.jsx)(lt,{entry:e,value:i.data,busy:a,setBusy:o,onSaved:r})]}):(0,x.jsx)(`p`,{role:`status`,children:`正在读取入口设置…`})]})}function lt({entry:e,value:t,busy:n,setBusy:r,onSaved:i}){let[a,o]=(0,l.useState)(typeof t.pace==`string`?t.pace:`custom`),s=typeof t.pace==`string`?it[t.pace].value:t.pace,[c,u]=(0,l.useState)({count:String(s.count),idle_seconds:String(s.idle_seconds),max_wait_seconds:String(s.max_wait_seconds)}),[d,f]=(0,l.useState)({min_chars:String(t.filters.min_chars),mention_only:t.filters.mention_only,context_messages:String(t.filters.context_messages),max_batches_per_hour:String(t.filters.max_batches_per_hour)}),[p,m]=(0,l.useState)(``),h=[[`min_chars`,`最短字数`,32768],[`context_messages`,`前后各 K 条消息`,100],[`max_batches_per_hour`,`每小时最多批次`,1e3]],y=[[`count`,`触发条数`,1e3],[`idle_seconds`,`空闲秒数`,86400],[`max_wait_seconds`,`最长等待秒数`,604800]];async function b(t){if(t.preventDefault(),!n){for(let[e,t,n]of h){let r=Number(d[e]);if(!d[e].trim()||!Number.isInteger(r)||r<0||r>n){m(`${t}须为 0—${n} 的整数`);return}}if(a===`custom`)for(let[e,t,n]of y){let r=Number(c[e]);if(!c[e].trim()||!Number.isInteger(r)||r<1||r>n){m(`${t}须为 1—${n} 的整数`);return}}r(!0),m(``);try{await g(`/entries/${encodeURIComponent(e.id)}/settings`,_(`PATCH`,{pace:a===`custom`?{count:Number(c.count),idle_seconds:Number(c.idle_seconds),max_wait_seconds:Number(c.max_wait_seconds)}:a,filters:{min_chars:Number(d.min_chars),mention_only:d.mention_only,context_messages:Number(d.context_messages),max_batches_per_hour:Number(d.max_batches_per_hour)}})),i()}catch(e){m(v(e))}finally{r(!1)}}}return(0,x.jsxs)(`form`,{className:`entry-settings-form`,onSubmit:b,noValidate:!0,children:[p&&(0,x.jsx)(A,{error:!0,children:p}),(0,x.jsxs)(`fieldset`,{className:`lifecycle-fieldset`,disabled:n,children:[(0,x.jsxs)(`label`,{children:[`学习节奏`,(0,x.jsxs)(`select`,{value:a,onChange:e=>o(e.target.value),children:[Object.entries(it).map(([e,t])=>(0,x.jsx)(`option`,{value:e,children:t.name},e)),(0,x.jsx)(`option`,{value:`custom`,children:`自定义`})]})]}),a===`custom`?(0,x.jsx)(`div`,{className:`form-grid`,children:y.map(([e,t,n])=>(0,x.jsxs)(`label`,{children:[t,(0,x.jsx)(`input`,{type:`number`,min:1,max:n,step:1,required:!0,value:c[e],onChange:t=>u({...c,[e]:t.target.value})})]},e))}):(0,x.jsxs)(`p`,{className:`muted`,children:[it[a].value.count,` 条 / 空闲`,` `,it[a].value.idle_seconds,` 秒 / 最长等待`,` `,it[a].value.max_wait_seconds,` 秒`]}),(0,x.jsx)(`h3`,{children:`高流量入口过滤`}),(0,x.jsxs)(`label`,{children:[`最短字数`,(0,x.jsx)(`input`,{"aria-label":`最短字数`,"aria-describedby":`min-chars-help`,type:`number`,min:0,max:32768,step:1,required:!0,value:d.min_chars,onChange:e=>f({...d,min_chars:e.target.value})})]}),(0,x.jsx)(`small`,{id:`min-chars-help`,children:`0 表示关闭;按正文去掉首尾空白后的字符数计算。`}),(0,x.jsxs)(`label`,{className:`check-label`,children:[(0,x.jsx)(`input`,{type:`checkbox`,checked:d.mention_only,onChange:e=>f({...d,mention_only:e.target.checked})}),`只学习提到角色的消息`]}),(0,x.jsx)(`p`,{className:`muted`,children:`正文中出现角色名或已确认的角色别名才算提及;引用内容、发送者名字和“记住”本身不算。`}),(0,x.jsxs)(`label`,{children:[`前后各 K 条消息`,(0,x.jsx)(`input`,{"aria-label":`前后各 K 条消息`,"aria-describedby":`context-help`,type:`number`,min:0,max:100,step:1,required:!0,disabled:!d.mention_only,value:d.context_messages,onChange:e=>f({...d,context_messages:e.target.value})})]}),(0,x.jsx)(`small`,{id:`context-help`,children:`0 表示仅学习提及消息;按本入口原始消息顺序计数,等待后续消息或空闲后定案。`}),(0,x.jsxs)(`label`,{children:[`每小时最多批次`,(0,x.jsx)(`input`,{"aria-label":`每小时最多批次`,"aria-describedby":`hourly-help`,type:`number`,min:0,max:1e3,step:1,required:!0,value:d.max_batches_per_hour,onChange:e=>f({...d,max_batches_per_hour:e.target.value})})]}),(0,x.jsx)(`small`,{id:`hourly-help`,children:`0 表示关闭;按滚动 60 分钟内新组成的批次计数。立即学习也受此上限及提及窗口限制。`}),(0,x.jsx)(A,{children:`设置立即作用于尚未冻结的待处理消息,已组成批次保持原样。被过滤的消息仍保存并可用于近期对话,但不会进入学习、不算积压或记忆缺口;放宽或关闭过滤不会重新学习这些消息。`})]}),(0,x.jsx)(`button`,{className:`primary`,disabled:n,children:n?`正在保存…`:`保存节奏与过滤`})]})}var ut={no_candidates:`没有需要判断的相关候选`,host_disabled:`本次请求已关闭判断`,configuration_disabled:`设置中已关闭判断`,unconfigured:`未配置判断模型`,configuration_error:`模型配置错误`,configuration:`模型配置或响应无效`,configuration_changed:`调用期间模型配置发生变化`,invalid_key:`密钥无效`,authentication:`密钥无效`,account_problem:`模型账户异常`,account:`模型账户异常`,usage_limit:`达到每日 token 上限`,rate_limited:`服务商限流,正在退避`,temporarily_unavailable:`模型用途已暂停`,paused:`模型用途已暂停`,queue_full:`判断排队已满`,queue_timeout:`排队超时`,timeout:`超过判断总预算`,retryable:`模型连接暂时失败`,invalid_output:`判断输出格式无效`,content_rejection:`模型拒绝判断内容`};function dt({value:e}){return e?(0,x.jsxs)(`div`,{className:`judgment-summary`,"aria-label":`本次召回判断`,children:[(0,x.jsxs)(`p`,{children:[(0,x.jsx)(`strong`,{children:`召回判断:`}),(0,x.jsx)(M,{tone:e.status===`degraded`?`danger`:e.status===`applied`?`green`:``,children:{applied:`正常`,disabled:`已关闭`,degraded:`降级`}[e.status]||e.status})]}),e.reason&&(0,x.jsx)(`p`,{children:ut[e.reason]||`判断原因:${e.reason}`}),(0,x.jsxs)(`p`,{children:[`被判断去掉 `,e.removed_memory_ids.length,` 条记忆`]}),!!e.stale_memory_ids?.length&&(0,x.jsxs)(`p`,{children:[`另有 `,e.stale_memory_ids.length,` 条记忆在复核时失效`]}),(0,x.jsxs)(`p`,{className:`muted`,children:[`耗时 `,C(e.duration_ms),` · 总预算 `,e.budget_seconds,` `,`秒(含排队)`]}),e.status===`degraded`&&(0,x.jsx)(`p`,{children:`本次保留仍有效的原召回。`})]}):(0,x.jsx)(`p`,{className:`fine-print`,children:`本次未返回召回判断状态。`})}function ft({value:e}){return e?(0,x.jsxs)(`div`,{className:`subject-annotations`,children:[e.possible_same_as.map(e=>(0,x.jsxs)(`p`,{children:[`可能是同一人:`,e.subjects.map(e=>e.name).join(`、`),` · 相信程度`,` `,e.belief,` / 100。`,(0,x.jsx)(`a`,{href:`#/people?id=${encodeURIComponent(e.subjects[0].id)}`,children:`核对人物联系`})]},e.link_id)),e.roleplay.map(e=>(0,x.jsxs)(`p`,{children:[`扮演关系(虚构):`,e.actor.name,` 扮演 `,e.character.name,` ·`,` `,e.worlds.map(e=>e??`场景未知`).join(`、`),` · 相信程度`,` `,e.belief,` / 100`]},e.link_id))]}):null}var pt={open:`未结束`,completed:`已完成`,abandoned:`已放弃`},mt={normal:`普通目标`,question:`询问`},ht={internal:`内部`,host:`宿主`,admin:`管理员`},gt={soon:`临近提醒`,due:`到期提醒`,overdue:`过期提醒`,immediate:`即时提醒`},_t={pending:`待取走`,taken:`已取走`,cancelled:`已取消`},vt={created:`新建`,possible_duplicate:`可能重复,已分别保留`,merged:`已合并`,pending:`去重待复核,已保留`},yt=(e,t)=>{let n=t?.people.find(t=>t.id===e);return n?`${n.name}(${e})`:e},bt=(e,t)=>e?t?.entries.find(t=>t.id===e)?.name||e:`未指定`;function xt({goal:e}){return(0,x.jsxs)(`div`,{className:`detail-badges goal-badges`,children:[(0,x.jsx)(M,{children:mt[e.kind]}),(0,x.jsx)(M,{children:pt[e.state]}),(0,x.jsx)(M,{children:ht[e.origin]}),e.overdue?(0,x.jsx)(M,{tone:`danger`,children:`已过期`}):e.due_soon?(0,x.jsx)(M,{tone:`purple`,children:`临近截止`}):null,e.possible_duplicate&&(0,x.jsx)(M,{tone:`warning`,children:`可能重复`}),e.basis_needs_review&&(0,x.jsx)(M,{tone:`warning`,children:`依据可能不成立`}),e.merged_into&&(0,x.jsx)(M,{children:`已合并`})]})}function St({goal:e}){return e.kind===`question`?(0,x.jsx)(x.Fragment,{children:`询问不设截止时间`}):e.deadline?e.deadline_unresolved?(0,x.jsxs)(`span`,{className:`danger-text`,children:[e.deadline,`(时间待明确,不安排提醒)`]}):(0,x.jsx)(N,{value:e.deadline}):(0,x.jsx)(x.Fragment,{children:`未设截止时间`})}function Ct({goal:e,catalog:t}){return(0,x.jsxs)(`dl`,{className:`goal-facts`,children:[(0,x.jsxs)(`div`,{children:[(0,x.jsx)(`dt`,{children:`截止时间`}),(0,x.jsx)(`dd`,{children:(0,x.jsx)(St,{goal:e})})]}),(0,x.jsxs)(`div`,{children:[(0,x.jsx)(`dt`,{children:`提醒提前量`}),(0,x.jsx)(`dd`,{children:e.kind===`question`?`询问不安排提醒`:`${e.effective_reminder_minutes} 分钟${e.reminder_minutes===null?`(使用默认值)`:``}`})]}),(0,x.jsxs)(`div`,{children:[(0,x.jsx)(`dt`,{children:`涉及的人`}),(0,x.jsx)(`dd`,{children:e.people.map(e=>yt(e,t)).join(`、`)||`未指定`})]}),(0,x.jsxs)(`div`,{children:[(0,x.jsx)(`dt`,{children:`产生入口`}),(0,x.jsx)(`dd`,{children:bt(e.entry_id,t)})]})]})}function wt({receipt:e,openGoal:t}){return(0,x.jsxs)(`div`,{className:`notice`,role:`status`,children:[(0,x.jsxs)(`p`,{children:[`已创建目标 #`,e.submitted_id,` · `,vt[e.dedup.status]]}),(0,x.jsxs)(`button`,{className:`text-button`,onClick:()=>t(e.goal.id),children:[`查看目标 #`,e.goal.id]}),e.dedup.target_id&&e.dedup.target_id!==e.goal.id&&(0,x.jsxs)(`button`,{className:`text-button`,onClick:()=>t(e.dedup.target_id),children:[`查看可能重复的目标 #`,e.dedup.target_id]})]})}function Tt({goal:e,locked:t,openMemory:n,clear:r}){return e.basis_annotations?.length?(0,x.jsxs)(`section`,{className:`panel goals-panel`,"aria-label":`目标依据复核`,children:[(0,x.jsx)(`h2`,{children:`依据可能不成立`}),(0,x.jsx)(`p`,{className:`goal-help`,children:`依据记忆已变化,请核对目标是否仍然成立;标注不会自动结束目标。`}),e.basis_annotations.map(i=>(0,x.jsxs)(`article`,{className:`goal-operation`,children:[(0,x.jsx)(`p`,{className:`report-text`,children:i.text}),(0,x.jsxs)(`p`,{children:[`依据修订 `,i.basis_revision,` · 观察到修订`,` `,i.observed_revision??`未知`]}),(0,x.jsxs)(`p`,{className:`muted`,children:[`复核时间:`,(0,x.jsx)(N,{value:i.created_at})]}),(0,x.jsxs)(`div`,{className:`actions`,children:[(0,x.jsxs)(`button`,{className:`text-button`,onClick:()=>n(i.memory_id),children:[`查看依据记忆 #`,i.memory_id]}),i.observed_merged_into!=null&&(0,x.jsxs)(`button`,{className:`text-button`,onClick:()=>n(i.observed_merged_into),children:[`已合并到记忆 #`,i.observed_merged_into]}),!e.merged_into&&(0,x.jsxs)(`button`,{disabled:t,onClick:()=>r(i),children:[`清除依据标注 #`,i.id]})]})]},i.id))]}):null}function Et({goal:e,openGoal:t}){let n=e.dedup_review;return(0,x.jsxs)(`section`,{className:`panel goals-panel`,"aria-label":`目标去重判断`,children:[(0,x.jsx)(`h2`,{children:`目标去重判断`}),n?(0,x.jsxs)(x.Fragment,{children:[(0,x.jsx)(M,{tone:n.state===`done`?`green`:`warning`,children:{pending:`待复核`,running:`复核中`,done:`已判断`,cancelled:`已取消`}[n.state]}),(0,x.jsxs)(`p`,{children:[`方法 `,n.method,` · 输入修订 `,n.input_revision,` · 尝试`,` `,n.attempts,` 次`]}),n.result.status&&(0,x.jsxs)(`p`,{children:[`判断结果:`,vt[n.result.status]||n.result.status]}),n.result.reason&&(0,x.jsx)(`p`,{children:pe(n.result.reason)}),n.result.target_id!=null&&(0,x.jsxs)(`button`,{className:`text-button`,onClick:()=>t(n.result.target_id),children:[`查看判断关联目标 #`,n.result.target_id]}),n.next_attempt_at&&(0,x.jsxs)(`p`,{children:[`下次复核:`,(0,x.jsx)(N,{value:n.next_attempt_at})]}),(0,x.jsxs)(`p`,{className:`muted`,children:[`最后更新:`,(0,x.jsx)(N,{value:n.updated_at})]}),[`pending`,`running`].includes(n.state)&&(0,x.jsx)(`p`,{className:`goal-help`,children:`已保留目标,等待模型复核;待复核不表示已经判定重复。`})]}):(0,x.jsx)(`p`,{className:`goal-help`,children:`没有异步复核任务记录。可能重复关系以当前目标标记为准。`}),(0,x.jsx)(`p`,{className:`goal-help`,children:`管理员手动创建的目标不会自动合并,判断为重复也只标记“可能重复”。`})]})}var Dt=e=>{let t=new Date(e);if(!Number.isFinite(t.getTime()))return``;let n=e=>String(e).padStart(2,`0`);return`${t.getFullYear()}-${n(t.getMonth()+1)}-${n(t.getDate())}T${n(t.getHours())}:${n(t.getMinutes())}:${n(t.getSeconds())}`};function Ot({goal:e,catalog:t,busy:n,locked:r=!1,submit:i,cancel:a}){let[o,s]=(0,l.useState)(e?.content||``),[c,u]=(0,l.useState)(e?.kind||`normal`),[d,f]=(0,l.useState)(e?`keep`:`none`),[p,m]=(0,l.useState)(``),[h,g]=(0,l.useState)(e?.deadline&&!e.deadline_unresolved?Dt(e.deadline):``),[_,v]=(0,l.useState)(e?.reminder_minutes?.toString()??``),[y,b]=(0,l.useState)([]),[S,C]=(0,l.useState)(``),[w,T]=(0,l.useState)(``);function E(t){if(t.preventDefault(),n||r)return;if(T(``),!o.trim()||o.trim().length>4e3){T(`目标正文须为 1—4000 个字符且不为空白`);return}let a={content:o.trim()};if(c===`normal`){if(_.trim()&&(!Number.isInteger(Number(_))||Number(_)<0||Number(_)>525600)){T(`提醒提前量须为 0—525600 的整数,留空使用默认值`);return}if((!e||_!==(e.reminder_minutes?.toString()??``))&&(a.reminder_minutes=_.trim()?Number(_):null),d===`date`){if(!/^\d{4}-\d{2}-\d{2}$/.test(p)||!Number.isFinite(Date.parse(p))||new Date(p).toISOString().slice(0,10)!==p){T(`请选择有效的截止日期`);return}a.deadline=p}else if(d===`datetime`){let e=new Date(h);if(!h||!Number.isFinite(e.getTime())||Dt(e.toISOString())!==(h.length===16?h+`:00`:h)){T(`请选择有效的截止时间`);return}a.deadline=e.toISOString()}else d===`none`&&(a.deadline=null)}else e||(a.deadline=null,a.reminder_minutes=null);if(!e){if(y.length>100){T(`涉及的人最多 100 位`);return}Object.assign(a,{kind:c,people:y,entry_id:S||null})}i(a)}return(0,x.jsxs)(`form`,{className:`goal-form`,onSubmit:E,noValidate:!0,children:[w&&(0,x.jsx)(A,{error:!0,children:w}),(0,x.jsxs)(`fieldset`,{className:`lifecycle-fieldset`,disabled:n||r,children:[!e&&(0,x.jsxs)(`label`,{children:[`创建类型`,(0,x.jsxs)(`select`,{value:c,onChange:e=>u(e.target.value),children:[(0,x.jsx)(`option`,{value:`normal`,children:`普通目标`}),(0,x.jsx)(`option`,{value:`question`,children:`询问`})]})]}),(0,x.jsxs)(`label`,{children:[`目标正文`,(0,x.jsx)(`textarea`,{required:!0,maxLength:4e3,rows:4,value:o,onChange:e=>s(e.target.value)})]}),c===`question`?(0,x.jsx)(`p`,{className:`lifecycle-help`,children:`询问没有截止时间和提醒,由宿主决定何时询问。`}):(0,x.jsxs)(x.Fragment,{children:[e&&(0,x.jsxs)(`p`,{children:[`当前截止时间:`,(0,x.jsx)(St,{goal:e})]}),(0,x.jsxs)(`div`,{className:`form-grid`,children:[(0,x.jsxs)(`label`,{children:[`截止方式`,(0,x.jsxs)(`select`,{value:d,onChange:e=>f(e.target.value),children:[e&&(0,x.jsx)(`option`,{value:`keep`,children:`保持当前截止时间`}),(0,x.jsx)(`option`,{value:`none`,children:`不设截止时间`}),(0,x.jsx)(`option`,{value:`date`,children:`指定日期`}),(0,x.jsx)(`option`,{value:`datetime`,children:`指定日期和时间`})]})]}),(0,x.jsxs)(`label`,{children:[`提醒提前量(分钟)`,(0,x.jsx)(`input`,{type:`number`,min:0,max:525600,step:1,value:_,placeholder:`留空使用默认值`,onChange:e=>v(e.target.value)})]}),d===`date`&&(0,x.jsxs)(`label`,{children:[`截止日期`,(0,x.jsx)(`input`,{"aria-label":`截止日期`,type:`date`,required:!0,value:p,onChange:e=>m(e.target.value)}),(0,x.jsx)(`small`,{children:`按角色时区当天 23:59:59 截止。`})]}),d===`datetime`&&(0,x.jsxs)(`label`,{children:[`截止日期和时间`,(0,x.jsx)(`input`,{"aria-label":`截止日期和时间`,type:`datetime-local`,step:1,required:!0,value:h,onChange:e=>g(e.target.value)}),(0,x.jsxs)(`small`,{children:[`按本机时区`,` `,Intl.DateTimeFormat().resolvedOptions().timeZone,` `,`输入;保存后按角色时区显示。`]})]})]}),(0,x.jsx)(`p`,{className:`lifecycle-help`,children:`留空提前量使用默认值;0 表示到期时提醒。修改期限或提前量会更新尚未取走的提醒,已取走的记录保留。`})]}),!e&&(0,x.jsxs)(`div`,{className:`form-grid`,children:[(0,x.jsxs)(`label`,{children:[`涉及的人`,(0,x.jsx)(`select`,{"aria-label":`涉及的人`,multiple:!0,size:4,value:y,onChange:e=>b(Array.from(e.target.selectedOptions,e=>e.value)),children:t?.people.filter(e=>![`self`,`scene`].includes(e.id)&&![`self`,`scene`].includes(e.kind||``)).map(e=>(0,x.jsx)(`option`,{value:e.id,children:yt(e.id,t)},e.id))}),(0,x.jsx)(`small`,{children:`可多选,也可不指定。`})]}),(0,x.jsxs)(`label`,{children:[`记录产生入口`,(0,x.jsxs)(`select`,{"aria-label":`记录产生入口`,value:S,onChange:e=>C(e.target.value),children:[(0,x.jsx)(`option`,{value:``,children:`不指定`}),t?.entries.map(e=>(0,x.jsxs)(`option`,{value:e.id,children:[e.name,` · `,e.id]},e.id))]}),(0,x.jsx)(`small`,{children:`仅记录产生位置,所有入口共享此目标。`})]})]}),!e&&(0,x.jsx)(`p`,{className:`lifecycle-help`,children:`管理员创建的目标独立保留;发现相似项只标“可能重复”,由管理员明确合并或驳回。`}),(0,x.jsx)(`div`,{className:`actions`,children:(0,x.jsx)(`button`,{className:`primary`,children:n?`正在保存…`:e?`保存目标`:`创建`})})]}),(0,x.jsx)(`button`,{type:`button`,className:`secondary`,disabled:n,onClick:a,children:`取消`})]})}function kt({note:e,openGoal:t}){return(0,x.jsxs)(`article`,{className:`notification-card`,children:[(0,x.jsxs)(`div`,{className:`panel-heading`,children:[(0,x.jsx)(`h3`,{children:gt[e.reminder_kind]}),(0,x.jsx)(M,{tone:e.status===`pending`?`purple`:``,children:_t[e.status]})]}),(0,x.jsx)(`p`,{className:`goal-content`,children:e.content}),(0,x.jsxs)(`div`,{className:`goal-time-lines`,children:[(0,x.jsxs)(`p`,{children:[`发布时间:`,(0,x.jsx)(N,{value:e.published_at})]}),(0,x.jsxs)(`p`,{children:[`计划时间:`,(0,x.jsx)(N,{value:e.scheduled_at})]}),e.deadline_at&&(0,x.jsxs)(`p`,{children:[`发布时的截止时间:`,(0,x.jsx)(N,{value:e.deadline_at})]}),e.taken_at&&(0,x.jsxs)(`p`,{children:[`取走时间:`,(0,x.jsx)(N,{value:e.taken_at})]}),e.cancelled_at&&(0,x.jsxs)(`p`,{children:[`取消时间:`,(0,x.jsx)(N,{value:e.cancelled_at})]})]}),(0,x.jsxs)(`button`,{className:`text-button`,onClick:()=>t(e.goal_id),children:[`查看目标 #`,e.goal_id]})]})}function At({goalId:e,openGoal:t}){let[n,r]=(0,l.useState)(e?``:`pending`),[i,a]=(0,l.useState)(0),o=new URLSearchParams({limit:`30`,offset:String(i)});n&&o.set(`status`,n),e&&o.set(`goal_id`,String(e));let s=y(`/notifications?${o}`,i===0?2e3:0);return(0,x.jsxs)(`section`,{className:`panel goals-panel`,"aria-label":`提醒列表`,children:[(0,x.jsxs)(`div`,{className:`panel-heading`,children:[(0,x.jsx)(`h2`,{children:e?`该目标的提醒`:`提醒`}),(0,x.jsx)(`button`,{className:`text-button`,onClick:s.refresh,children:`刷新提醒`})]}),(0,x.jsx)(`p`,{className:`goal-help`,children:`“已取走”只表示宿主拿到了提醒,不代表已送达用户或目标完成。此处查看不会取走提醒。`}),(0,x.jsxs)(`label`,{className:`notification-filter`,children:[`提醒状态`,(0,x.jsxs)(`select`,{value:n,onChange:e=>{r(e.target.value),a(0)},children:[(0,x.jsx)(`option`,{value:``,children:`全部`}),Object.entries(_t).map(([e,t])=>(0,x.jsx)(`option`,{value:e,children:t},e))]})]}),s.error&&(0,x.jsxs)(A,{error:!0,children:[s.error,`。`,s.data&&`以下为上次读取结果。`]}),!s.data&&!s.error&&(0,x.jsx)(`p`,{role:`status`,children:`正在读取提醒…`}),s.data?.items.length===0&&(0,x.jsx)(j,{title:`当前范围没有提醒`,children:`有截止时间的普通目标会在临近、到期时产生提醒;询问不安排提醒。`}),s.data?.items.map(e=>(0,x.jsx)(kt,{note:e,openGoal:t},e.id)),s.data&&(0,x.jsx)(T,{total:s.data.total,offset:i,change:a})]})}var jt=`合并待核对(双方互有细节,不代表事实矛盾)`;function Mt({details:e,ids:t,openMemory:n,disabled:r=!1}){return(0,x.jsxs)(`div`,{className:`merge-detail-review`,children:[(0,x.jsx)(`p`,{children:`双方各有另一条没有的细节,未自动合并。采纳只确认建议,不执行合并,也不改正文和相信程度。`}),[`a`,`b`].map((i,a)=>(0,x.jsxs)(`div`,{children:[t[a]==null?(0,x.jsxs)(`strong`,{children:[`第 `,a+1,` 条记忆的独有细节`]}):(0,x.jsxs)(`button`,{disabled:r,className:`text-button`,onClick:()=>n(t[a]),children:[`记忆 #`,t[a],` 的独有细节`]}),e?.[i]?.length?(0,x.jsx)(`ul`,{children:e[i].map((e,t)=>(0,x.jsx)(`li`,{children:e},t))}):(0,x.jsx)(`p`,{className:`muted`,children:`未保存独有细节,请核对报告正文。`})]},i))]})}var Nt={relationship:`与我的关系`,fact:`稳定事实`,preference:`偏好与习惯`,recent:`近期重要事项`},Pt={published:`已发布`,candidate:`待确认`,rejected:`已拒绝`,failed:`失败`,skipped:`已跳过`,conflict:`版本或依据冲突`,running:`生成中`};function Ft(e){return{memory_purged:`依据记忆已彻底清除`,memory_unavailable:`依据记忆已遗忘或删除`,visibility_changed:`依据不再满足全部入口共享条件`,no_longer_related:`依据不再与此人物相关`,memory_changed:`依据记忆已修订`,subject_changed:`人物资料或关联发生变化`,subject_merged:`人物已合并`,subject_unavailable:`此人物当前不可生成摘要`,subject_kind:`此类主体不生成人物摘要`,time_range_expired:`近期事项已到期`,purged:`因彻底清除而清空`,disabled:`人物摘要开关已关闭`,insufficient_memories:`相关全局有效记忆不足(至少需要 8 条)`,no_changes_since_attempt:`上次被拒或确定性失败后,尚无新依据`,not_due:`尚未达到更新条件`,generation_in_progress:`管理员生成中,本轮已跳过`,interrupted:`任务被中断,可以重试`,invalid_output:`模型输出格式无效`,invalid_check_output:`模型检查格式无效`,invalid_check_item:`逐条检查格式无效`,too_many_deleted:`检查累计删去超过一半的模型条目`,empty:`没有可保留的摘要内容`,too_many_items:`条目数超出预算`,too_long:`摘要字数超出预算`,token_budget:`摘要 token 超出预算`,invalid_item_or_references:`条目或引用依据无效`,missing_time_range:`近期事项缺少时间范围`,invalid_or_expired_time_range:`时间范围无效或已经到期`,unexpected_time_range:`非近期事项带有不适用的时间范围`,too_many_relationships:`与我的关系最多保留一条`,timeout:`模型请求超时,可以重试`,retryable:`模型暂时失败,可以重试`,temporary_unavailable:`模型暂时不可用,可以稍后重试`,paused:`模型用途已暂停`,usage_limit:`已达到每日用量上限`,call_budget:`剩余模型调用预算不足`,unconfigured:`模型尚未配置`,configuration:`模型配置无效`,account:`模型账户不可用`,refused:`模型拒绝处理`,unexpected_error:`生成过程中发生错误,可以重试`,superseded:`已被新候选或编辑取代`,administrator_rejected:`管理员拒绝`,evidence_changed:`依据已变化,请刷新后重试`,current_version_changed:`当前版本已变化,请刷新后重试`,settings_or_subject_changed:`设置或人物资料已变化,请刷新后重试`,settings_changed:`设置已变化,请刷新后重试`,candidate_changed:`候选已变化,请刷新后重试`}[e]||e}var It=(e,t)=>`#/people?id=${encodeURIComponent(e)}${t?`&summary_version=${t}`:``}`;function Lt({basis:e,catalog:t,openMemory:n}){let r=y(e.memory?`/memories/${e.memory_id}`:null,5e3),i=r.data,a=[...new Set([i?.entry_id,...(i?.sources||[]).map(e=>e.message?.entry_id)].filter(e=>!!e))];return(0,x.jsxs)(`article`,{className:`persona-evidence`,children:[(0,x.jsxs)(`h4`,{children:[`记忆 #`,e.memory_id]}),(0,x.jsxs)(`p`,{className:`muted`,children:[`记录修订 `,e.revision,` ·`,` `,e.memory?`当前修订 ${e.memory.revision} · ${E(e.memory.lifecycle)}`:`当前记忆不可用`]}),(0,x.jsx)(`p`,{className:`persona-content`,children:e.content_at_revision??`该修订正文不可用`}),i&&(0,x.jsxs)(x.Fragment,{children:[(0,x.jsxs)(`p`,{className:`muted`,children:[`当前记忆记录时间:`,(0,x.jsx)(N,{value:i.created_at}),`;事件时间:`,(0,x.jsx)(N,{value:i.event_time??null})]}),(0,x.jsxs)(`p`,{className:`muted`,children:[`当前来源入口类型:`,a.length?a.map(e=>{let n=t?.entries.find(t=>t.id===e);return n?`${{group:`群聊`,private:`私聊`}[n.kind]||n.kind} · ${n.name}`:`入口 ${e}(类型暂不可用)`}).join(`;`):i.sources.some(e=>e.kind===`initial_setting`)?`初始设定`:`未记录入口`]}),(0,x.jsx)(`small`,{children:`以上时间和入口来自当前记忆详情,不是该版本生成时的历史快照。`})]}),r.loading&&(0,x.jsx)(`p`,{children:`正在读取当前记忆元数据…`}),r.error&&(0,x.jsxs)(A,{error:!0,children:[r.error,(0,x.jsx)(`button`,{onClick:r.refresh,children:`重试记忆元数据`})]}),(0,x.jsxs)(`button`,{className:`text-button`,disabled:!e.memory,onClick:()=>n(e.memory_id),children:[`查看记忆 #`,e.memory_id]})]})}function Rt({sentence:e,index:t,catalog:n,openMemory:r}){let[i,a]=(0,l.useState)(!1),o=e.admin_written||e.origin===`admin`;return(0,x.jsxs)(`li`,{className:`persona-sentence`,children:[(0,x.jsxs)(`div`,{className:`persona-sentence-line`,children:[(0,x.jsxs)(`span`,{className:`muted`,children:[t+1,`.`]}),(0,x.jsx)(`p`,{children:e.text}),(0,x.jsx)(M,{tone:o?`warning`:``,children:o?`手写`:re[e.origin]||e.origin})]}),(0,x.jsxs)(`button`,{className:`text-button`,"aria-expanded":i,onClick:()=>a(!i),children:[i?`收起`:`展开`,`第 `,t+1,` 句依据`]}),i&&(0,x.jsxs)(`div`,{className:`persona-basis`,children:[(0,x.jsxs)(`p`,{children:[`该句的依据日期:`,e.dates.length?e.dates.join(`、`):`未记录日期`,`(`,e.date_count,` 个日期)`,e.initial_setting&&` · 含初始设定`]}),e.basis.map(e=>(0,x.jsx)(Lt,{basis:e,catalog:n,openMemory:r},`${e.memory_id}-${e.revision}`)),!e.basis.length&&(0,x.jsx)(`p`,{className:`muted`,children:o?`管理员手写内容,没有记忆依据;手写标记不代表模型检查通过。`:`初始模板,没有逐句记忆依据。`})]})]})}var zt=e=>typeof e==`object`&&e&&!Array.isArray(e)?e:null;function Bt({value:e,index:t}){let n=zt(e);if(!n)return(0,x.jsxs)(`li`,{children:[`第 `,t+1,` 句:检查结果格式不完整`]});let r=(e,t,n)=>typeof e==`boolean`?e?t:n:`未报告`;return(0,x.jsxs)(`li`,{children:[`第 `,typeof n.index==`number`?n.index:t+1,` `,`句:依据支持 `,r(n.supported,`是`,`否`),`;虚构`,` `,r(n.fabricated,`是`,`否`),`;场景限定`,` `,r(n.scene_qualified,`是`,`否`),typeof n.reason==`string`&&(0,x.jsx)(`p`,{children:n.reason}),Array.isArray(n.violations)&&n.violations.some(e=>typeof e==`string`)&&(0,x.jsxs)(`p`,{children:[`违反要求:`,n.violations.filter(e=>typeof e==`string`).join(`;`)]})]})}function W({version:e}){let{checks:t}=e,n=zt(t.model);return(0,x.jsxs)(`div`,{className:`persona-checks`,children:[(0,x.jsx)(`h3`,{children:`检查结果`}),(0,x.jsxs)(`p`,{children:[t.passed===!0?`综合检查通过`:t.passed===!1?`综合检查未通过`:`没有综合检查结论`,t.administrator_published&&` · 管理员直接发布`]}),(0,x.jsx)(`p`,{className:`muted`,children:`手写标记表示内容的来源,不代表模型检查通过。`}),(0,x.jsx)(`h4`,{children:`确定性检查`}),(0,x.jsx)(`p`,{children:t.deterministic?.passed===!0?`通过`:t.deterministic?.passed===!1?`未通过`:`没有确定性检查记录`}),!!t.deterministic?.errors?.length&&(0,x.jsx)(`ul`,{children:t.deterministic.errors.map((e,t)=>(0,x.jsx)(`li`,{children:ae(e)},t))}),!!t.deterministic?.warnings?.length&&(0,x.jsx)(`ul`,{className:`muted`,children:t.deterministic.warnings.map((e,t)=>(0,x.jsxs)(`li`,{children:[`提示:`,ae(e)]},t))}),(0,x.jsx)(`h4`,{children:`模型检查`}),t.model==null?(0,x.jsx)(`p`,{children:`未进行模型检查`}):n?(0,x.jsxs)(x.Fragment,{children:[typeof n.change_degree==`string`&&(0,x.jsxs)(`p`,{children:[`判断的变化程度:`,R[n.change_degree]||n.change_degree]}),typeof n.reason==`string`&&(0,x.jsx)(`p`,{children:n.reason}),Array.isArray(n.sentences)?(0,x.jsx)(`ul`,{children:n.sentences.map((e,t)=>(0,x.jsx)(Bt,{value:e,index:t},t))}):(0,x.jsx)(`p`,{children:`逐句检查结果格式不完整`})]}):(0,x.jsx)(`p`,{children:`模型检查结果格式不完整`}),!!t.model_errors?.length&&(0,x.jsx)(`ul`,{children:t.model_errors.map((e,t)=>(0,x.jsx)(`li`,{children:ae(e)},t))}),t.admin_content_removed_or_changed&&(0,x.jsx)(A,{children:`候选删去或改动了手写内容,变化程度按“大”处理。默认发布方式需要确认;“全部自动”模式会直接发布检查通过的候选。`})]})}function Vt({id:e,title:t,catalog:n,openMemory:r,onAction:i,locked:a=!1,currentID:o,history:s=!1}){let c=y(`/persona/versions/${e}`,5e3),l=c.data;return(0,x.jsxs)(`section`,{className:`panel persona-version`,"aria-label":t,children:[l&&(0,x.jsxs)(`h2`,{children:[t,` · v`,e]}),c.error&&(0,x.jsxs)(A,{error:!0,children:[c.error,(0,x.jsx)(`button`,{onClick:c.refresh,children:`重试版本详情`})]}),l?(0,x.jsxs)(x.Fragment,{children:[(0,x.jsxs)(`div`,{className:`persona-meta`,children:[(0,x.jsx)(M,{tone:l.status===`pending`||l.status===`rejected`?`warning`:``,children:ne[l.status]}),(0,x.jsx)(`span`,{children:L[l.source]||l.source}),(0,x.jsxs)(`span`,{children:[`变化程度:`,l.change_degree?R[l.change_degree]||l.change_degree:`未记录`]})]}),(0,x.jsxs)(`p`,{className:`muted`,children:[`生成时间:`,(0,x.jsx)(N,{value:l.generated_at}),` · 发布时间:`,(0,x.jsx)(N,{value:l.published_at})]}),(0,x.jsxs)(`p`,{className:`muted`,children:[l.base_version_id?`基于 v${l.base_version_id}`:`初始版本`,l.rollback_of&&` · 回滚自 v${l.rollback_of}`]}),(0,x.jsx)(`ol`,{className:`persona-sentences`,children:l.sentences.map((e,t)=>(0,x.jsx)(Rt,{sentence:e,index:t,catalog:n,openMemory:r},`${t}-${e.text}`))}),!l.sentences.length&&(0,x.jsx)(`p`,{className:`persona-content`,children:l.content}),!!l.rejection_reasons.length&&(0,x.jsxs)(`div`,{className:`notice error`,children:[(0,x.jsx)(`h3`,{children:`拒绝原因`}),(0,x.jsx)(`ul`,{children:l.rejection_reasons.map((e,t)=>(0,x.jsx)(`li`,{children:ae(e)},t))})]}),(0,x.jsx)(W,{version:l}),l.settings&&(0,x.jsxs)(`details`,{children:[(0,x.jsx)(`summary`,{children:`本次候选接受时的设置`}),(0,x.jsxs)(`p`,{children:[`发布方式:`,{small_medium_auto:`小或中自动发布`,all_auto:`全部自动`,all_manual:`全部人工确认`}[l.settings.publish_mode]||l.settings.publish_mode]}),(0,x.jsx)(`h4`,{children:`生成目标`}),(0,x.jsx)(`p`,{className:`persona-content`,children:l.settings.goal}),(0,x.jsx)(`h4`,{children:`监管要求`}),(0,x.jsx)(`p`,{className:`persona-content`,children:l.settings.rules})]}),(0,x.jsxs)(`div`,{className:`persona-actions`,children:[i&&o&&l.status===`pending`&&(0,x.jsxs)(x.Fragment,{children:[(0,x.jsx)(`button`,{className:`primary`,disabled:a||!!c.error,onClick:()=>i(`confirm`,l),children:`确认发布候选`}),(0,x.jsx)(`button`,{disabled:a||!!c.error,onClick:()=>i(`reject`,l),children:`拒绝候选`})]}),i&&o&&s&&[`current`,`history`].includes(l.status)&&(0,x.jsx)(`button`,{disabled:a||!!c.error,onClick:()=>i(`rollback`,l),children:`回滚到此版本`}),(0,x.jsx)(`a`,{href:`#/operations?object_type=persona&object_id=${e}`,children:`查看版本操作记录`})]})]}):(0,x.jsx)(`p`,{children:`正在读取版本详情…`})]})}function Ht({sentences:e,start:t}){return e.length?(0,x.jsx)(`ol`,{className:`persona-diff-sentences`,start:t,children:e.map((e,t)=>(0,x.jsxs)(`li`,{children:[(0,x.jsx)(`p`,{children:e.text}),(0,x.jsxs)(`small`,{children:[re[e.origin]||e.origin,` · 依据:`,e.basis.length?e.basis.map(e=>`#${e.memory_id} 修订 ${e.revision}`).join(`、`):`无`,` `,`· 日期:`,e.dates.join(`、`)||`未记录`,`(`,e.date_count,` 个日期)`,e.initial_setting&&` · 初始设定`]})]},t))}):(0,x.jsx)(`p`,{className:`muted`,children:`无句子`})}function Ut({before:e,after:t}){let n=y(`/persona/diff?before_version=${e}&after_version=${t}`);return(0,x.jsxs)(`section`,{className:`persona-diff`,"aria-label":`版本差异 v${e} 到 v${t}`,children:[(0,x.jsxs)(`h3`,{children:[`逐句差异:v`,e,` → v`,t]}),n.error&&(0,x.jsxs)(A,{error:!0,children:[n.error,(0,x.jsx)(`button`,{onClick:n.refresh,children:`重试版本对比`})]}),!n.data&&!n.error&&(0,x.jsx)(`p`,{children:`正在读取差异…`}),n.data?.changes.length===0&&(0,x.jsx)(`p`,{children:`这两个版本的句子与依据没有差异。`}),n.data?.changes.map((n,r)=>(0,x.jsxs)(`article`,{className:`persona-change persona-change-${n.kind}`,children:[(0,x.jsx)(`h4`,{children:{insert:`新增句子`,delete:`删除句子`,replace:`改写句子`,basis_changed:`正文相同,依据或归属变化`}[n.kind]}),(0,x.jsxs)(`div`,{className:`persona-diff-columns`,children:[(0,x.jsxs)(`div`,{children:[(0,x.jsxs)(`strong`,{children:[`之前 · v`,e]}),(0,x.jsx)(Ht,{sentences:n.before,start:n.before_start})]}),(0,x.jsxs)(`div`,{children:[(0,x.jsxs)(`strong`,{children:[`之后 · v`,t]}),(0,x.jsx)(Ht,{sentences:n.after,start:n.after_start})]})]})]},r))]})}function Wt({value:e,openMemory:t}){return(0,x.jsxs)(`details`,{className:`source consolidation-excerpts`,children:[(0,x.jsx)(`summary`,{children:`查看来源摘录`}),(0,x.jsx)(`p`,{className:`fine-print`,children:`本次模型判断时的来源摘录;完整来源与前后文可在记忆详情查看。`}),!e?.length&&(0,x.jsx)(`p`,{children:`没有保存的来源摘录。`}),e?.map((e,n)=>(0,x.jsxs)(`div`,{children:[(0,x.jsxs)(`button`,{className:`text-button`,onClick:()=>t(e.memory_id),children:[`记忆 #`,e.memory_id]}),!e.sources.length&&(0,x.jsx)(`p`,{className:`quiet`,children:`没有可用的来源消息摘录。`}),e.sources.map(e=>(0,x.jsxs)(`article`,{className:`consolidation-excerpt`,children:[(0,x.jsxs)(`p`,{className:`muted`,children:[`消息 #`,e.id,` · `,e.entry_name||e.entry_id,` ·`,` `,{private:`私聊`,group:`群聊`,trial:`试用`}[e.entry_kind||``]||e.entry_kind,` `,`· `,e.sender_name||e.sender,` ·`,` `,(0,x.jsx)(N,{value:e.at||e.occurred_at||null})]}),(0,x.jsx)(`p`,{className:`fine-print`,children:{message:`他人消息`,self_output:`角色输出`,action_result:`行动结果`,event:`事件`}[e.kind]||e.kind}),(0,x.jsx)(`blockquote`,{children:e.text}),e.quote_content&&(0,x.jsxs)(`div`,{className:`source-quote`,children:[(0,x.jsxs)(`small`,{children:[`原消息引用 `,e.quote_author||``]}),(0,x.jsx)(`blockquote`,{children:e.quote_content})]})]},e.id))]},`${e.memory_id}:${n}`))]})}function Gt(e){let t=e.details;return e.outcome===`auto_conflict`?z[t.relation||``]||t.relation:e.outcome===`auto_dependency`?B[t.verdict||``]||t.verdict:e.outcome===`protected`?pe(`protected`):`未合并:${pe(e.reason||``)}`}function Kt({runId:e,item:t,refresh:n}){let r=t.details.undo,[i,a]=(0,l.useState)(!1),[o,s]=(0,l.useState)(``);if(!r)return null;if(r.undone_at)return(0,x.jsxs)(`p`,{className:`muted`,children:[`已撤销 · `,(0,x.jsx)(N,{value:r.undone_at})]});async function c(){if(!i){a(!0),s(``);try{await g(`/maintenance/${e}/consolidation/${t.item_key}/undo`,_(`POST`,{expected_revision:r.expected_revision})),n?.()}catch(e){s(v(e))}finally{a(!1)}}}return(0,x.jsxs)(`div`,{className:`actions`,children:[(0,x.jsx)(`button`,{className:`text-button`,disabled:i,onClick:c,children:`撤销此次处理`}),o&&(0,x.jsx)(A,{error:!0,children:o})]})}function qt({name:e,items:t,openMemory:n,runId:r,refresh:i}){let[a,o]=(0,l.useState)(0);return(0,x.jsxs)(`section`,{className:`detail-section`,"aria-label":e,children:[(0,x.jsxs)(`h3`,{children:[e,` `,(0,x.jsx)(M,{children:t.length})]}),e.includes(`建议`)&&(0,x.jsx)(`p`,{className:`fine-print`,children:`仅管理员可见。采纳只记录确认,不改正文和相信程度;已确认的建议也不进入宿主召回。`}),!t.length&&(0,x.jsx)(`p`,{className:`quiet`,children:`本次暂无记录。`}),t.slice(a,a+30).map(e=>{let t=e.details;return(0,x.jsxs)(`article`,{className:`maintenance-item`,children:[e.outcome===`merged`?(0,x.jsxs)(x.Fragment,{children:[(0,x.jsxs)(`div`,{className:`actions`,children:[t.absorbed_ids?.map(e=>(0,x.jsxs)(`button`,{className:`text-button`,onClick:()=>n(e),children:[`记忆 #`,e]},e)),(0,x.jsx)(`span`,{children:`并入`}),t.result_id!=null&&(0,x.jsxs)(`button`,{className:`text-button`,onClick:()=>n(t.result_id),children:[`记忆 #`,t.result_id]})]}),(0,x.jsx)(`p`,{children:`全部来源已归入保留记忆;被合并的记忆保留为占位。`}),(0,x.jsxs)(`p`,{children:[`来源消息:`,t.source_messages?.map(e=>`#${e}`).join(`、`)||`无消息来源`,` `,`· 重定向派生依据 `,t.redirected_dependencies??0,` 处`]})]}):t.auto_resolution===`auto_v1`?(0,x.jsxs)(x.Fragment,{children:[(0,x.jsx)(M,{tone:`warning`,children:`自主处理`}),` `,(0,x.jsx)(`span`,{children:Gt(e)}),t.check_failure&&(0,x.jsxs)(`p`,{className:`muted`,children:[`核对未通过:`,t.check_failure]}),t.suggestions?.map(e=>(0,x.jsxs)(`p`,{children:[(0,x.jsx)(M,{children:ce[e.kind]||e.kind}),` `,(0,x.jsx)(M,{children:le[e.review_status]}),` `,e.text]},e.id))]}):(0,x.jsxs)(x.Fragment,{children:[(0,x.jsx)(M,{tone:`warning`,children:`模型建议`}),` `,(0,x.jsx)(`span`,{children:t.suggestion_type===`merge_details`?jt:e.outcome===`conflicts`?`矛盾检查`:`依赖复核`}),t.suggestion_type===`merge_details`&&(0,x.jsx)(Mt,{details:t.unique_details,ids:t.before_memories?.slice(0,2).map(e=>e.id)||[],openMemory:n}),t.suggestions?.map(e=>(0,x.jsxs)(`div`,{className:`suggestion-summary`,children:[(0,x.jsxs)(`p`,{children:[(0,x.jsx)(M,{children:t.suggestion_type===`merge_details`?`合并待核对`:ce[e.kind]||e.kind}),` `,(0,x.jsx)(M,{children:le[e.review_status]}),` 建议 #`,e.id]}),(0,x.jsx)(`p`,{children:e.text}),e.memory_id!=null&&(0,x.jsxs)(`button`,{className:`text-button`,onClick:()=>n(e.memory_id),children:[`查看记忆 #`,e.memory_id,` 的建议`]}),e.confirmed_at&&(0,x.jsxs)(`p`,{className:`muted`,children:[`管理员确认:`,(0,x.jsx)(N,{value:e.confirmed_at})]}),e.cleared_at&&(0,x.jsxs)(`p`,{className:`muted`,children:[`管理员清除:`,(0,x.jsx)(N,{value:e.cleared_at})]})]},e.id)),!t.suggestions?.length&&e.memory_id!=null&&(0,x.jsxs)(`button`,{className:`text-button`,onClick:()=>n(e.memory_id),children:[`查看记忆 #`,e.memory_id]})]}),t.report&&(0,x.jsx)(`p`,{className:`report-text`,children:t.report}),!!t.before_memories?.length&&(0,x.jsxs)(`details`,{className:`source`,children:[(0,x.jsx)(`summary`,{children:`涉及的记忆与修订快照`}),t.before_memories.map(e=>{let r=t.after_memories?.find(t=>t.id===e.id);return(0,x.jsxs)(`div`,{children:[(0,x.jsxs)(`button`,{className:`text-button`,onClick:()=>n(e.id),children:[`记忆 #`,e.id]}),(0,x.jsxs)(`span`,{children:[` `,`· 修订 `,e.revision,r&&` → ${r.revision}`]}),(0,x.jsx)(`p`,{className:`report-text`,children:e.content||`正文已不可用`}),r?.content!==e.content&&(0,x.jsxs)(`p`,{className:`report-text`,children:[`整理后:`,r?.content||`正文已不可用`]})]},e.id)})]}),(0,x.jsx)(Kt,{runId:r,item:e,refresh:i}),(0,x.jsx)(Wt,{value:t.source_excerpts,openMemory:n}),(0,x.jsx)(`small`,{children:(0,x.jsx)(N,{value:e.created_at})})]},`${e.phase}:${e.item_key}`)}),(0,x.jsx)(T,{total:t.length,offset:a,change:o})]})}function Jt({data:e}){let[t,n]=(0,l.useState)(0),r=e.items.filter(e=>e.outcome===`goals_reviewed`);return!e.goal_review&&!r.length?null:(0,x.jsxs)(`section`,{className:`detail-section`,"aria-label":`目标复核结果`,children:[(0,x.jsx)(`h3`,{children:`目标复核结果`}),(0,x.jsx)(`p`,{children:e.goal_review?.enabled===!1?`未执行:${pe(e.goal_review.skip_reason||`disabled`)}`:`已检查 ${e.goal_review?.checked??0} 个目标,${r.length} 个目标的依据标注发生变化。`}),(0,x.jsx)(`p`,{className:`fine-print`,children:`复核只更新依据标注,不自动完成或放弃目标,也不改变提醒。`}),r.slice(t,t+30).map(e=>(0,x.jsxs)(`article`,{className:`maintenance-item`,children:[(0,x.jsxs)(`a`,{href:`#/state?tab=goals&id=${e.object_id}`,children:[`查看目标 #`,e.object_id]}),(0,x.jsxs)(`p`,{children:[`新增或重新启用标注:`,e.details.annotation_ids?.map(e=>`#${e}`).join(`、`)||`无`]}),(0,x.jsxs)(`p`,{children:[`结束的旧标注:`,e.details.ended_annotation_ids?.map(e=>`#${e}`).join(`、`)||`无`]}),(0,x.jsx)(`small`,{children:(0,x.jsx)(N,{value:e.created_at})})]},e.item_key)),(0,x.jsx)(T,{total:r.length,offset:t,change:n})]})}function Yt({data:e}){let[t,n]=(0,l.useState)(0),r=e.items.filter(e=>e.outcome===`person_summary`);return!r.length&&e.consolidation?.settings.person_summary_enabled==null?null:(0,x.jsxs)(`section`,{className:`detail-section`,"aria-label":`人物摘要处理结果`,children:[(0,x.jsxs)(`h3`,{children:[`人物摘要处理结果 `,(0,x.jsx)(M,{children:r.length})]}),e.consolidation?.settings.person_summary_enabled===!1&&(0,x.jsx)(`p`,{children:`本次运行已关闭人物摘要更新。`}),!r.length&&(0,x.jsx)(`p`,{children:`本次没有人物摘要处理记录。`}),r.slice(t,t+30).map(e=>(0,x.jsxs)(`article`,{className:`maintenance-item`,children:[(0,x.jsx)(`p`,{children:(0,x.jsx)(M,{children:Pt[e.details.status||``]||e.details.status||`未记录结果`})}),e.details.subject_id?(0,x.jsxs)(`a`,{href:It(e.details.subject_id,e.details.version),children:[`查看人物 `,e.details.subject_id,e.details.version?` 的摘要 v${e.details.version}`:` 的摘要与任务`]}):(0,x.jsx)(`p`,{children:`人物摘要步骤异常,未关联具体人物。`}),e.reason&&(0,x.jsx)(`p`,{children:Ft(e.reason)}),e.details.attempt_id!=null&&(0,x.jsxs)(`p`,{children:[`生成任务 #`,e.details.attempt_id]}),(0,x.jsx)(`small`,{children:(0,x.jsx)(N,{value:e.created_at})})]},e.item_key)),(0,x.jsx)(T,{total:r.length,offset:t,change:n})]})}function Xt({data:e}){let t=e.items.filter(e=>e.outcome===`model_step_budget`),n=e.items.filter(e=>e.outcome===`backlog_trimmed`),r=e.question_expiry;return(0,x.jsxs)(x.Fragment,{children:[(e.consolidation||t.length>0)&&(0,x.jsxs)(`section`,{className:`detail-section`,"aria-label":`各步骤模型调用上限与用量`,children:[(0,x.jsx)(`h3`,{children:`各步骤模型调用上限与用量`}),(0,x.jsx)(`p`,{className:`fine-print`,children:`每一步独立计数,包含重试和修正;上限沿用本次运行接受时的设置。未到期、关闭或没有任务的步骤可能没有记录。`}),!t.length&&(0,x.jsx)(`p`,{className:`quiet`,children:`本次未记录步骤用量。`}),t.map(e=>(0,x.jsxs)(`article`,{className:`maintenance-item`,"aria-label":V[e.details.step||``]||e.details.step||`未知步骤`,children:[(0,x.jsx)(`h4`,{children:V[e.details.step||``]||e.details.step||`未知步骤`}),(0,x.jsxs)(`p`,{children:[`本轮上限:`,e.details.max_calls??`未记录`,` 次 · 已用:`,e.details.calls_used??`未记录`,` 次`]}),(0,x.jsxs)(`p`,{children:[`停止原因:`,e.reason?pe(e.reason):`未记录`]})]},e.item_key))]}),!!n.length&&(0,x.jsxs)(`section`,{className:`detail-section`,"aria-label":`配对积压修剪`,children:[(0,x.jsx)(`h3`,{children:`配对积压修剪`}),n.map(e=>(0,x.jsxs)(`p`,{children:[`配对积压修剪:`,e.details.count??`未记录`,` 项 · 待处理配对上限:`,e.details.limit??`未记录`,` 项`]},e.item_key)),(0,x.jsx)(`p`,{className:`fine-print`,children:`仅修剪待处理配对,不改变记忆;相关记忆发生变化后可重新规划。`})]}),r&&(0,x.jsxs)(`section`,{className:`detail-section`,"aria-label":`询问到期结束`,children:[(0,x.jsx)(`h3`,{children:`询问到期结束`}),(0,x.jsxs)(`p`,{children:[`到期自动结束的询问:`,r.count,` 条`]}),(0,x.jsx)(`p`,{className:`fine-print`,children:`仅学习产生且超过保留天数仍未结束的询问;宿主或管理员创建的询问和普通目标不受影响。`})]})]})}function Zt({data:e,openMemory:t,refresh:n}){let r=e.summary.model_calls,i=e.persona;return(0,x.jsxs)(x.Fragment,{children:[e.consolidation&&(0,x.jsxs)(`section`,{className:`detail-section`,"aria-label":`本次整理与模型用量`,children:[(0,x.jsx)(`h3`,{children:`本次整理与模型用量`}),(0,x.jsxs)(`p`,{children:[`后续待处理项目:`,e.consolidation.deferred,` 项`,e.consolidation.skip_reason&&` · ${pe(e.consolidation.skip_reason)}`]}),r&&(0,x.jsxs)(x.Fragment,{children:[(0,x.jsxs)(`p`,{children:[`模型调用 `,r.count,` 次 · 输入 `,r.prompt_tokens??0,` `,`token · 输出 `,r.completion_tokens??0,` token · 总耗时`,` `,C(r.duration_ms??0)]}),(0,x.jsxs)(`p`,{className:`fine-print`,children:[r.unknown_usage_calls??0,` `,`次调用用量未知;用量为四步骤已知合计,包含实际重试、入口画像、persona 及人物摘要调用。`]})]}),(0,x.jsxs)(`details`,{children:[(0,x.jsx)(`summary`,{children:`本次接受任务时的整理设置`}),ue.map(([t,n])=>(0,x.jsxs)(`p`,{children:[n,`:`,e.consolidation?.settings[t]??`未记录`]},t)),(0,x.jsxs)(`p`,{children:[`处理方式:`,e.consolidation?.settings.resolution===`auto_v1`?`自主处理`:`只报告`]}),fe.map(([t,n])=>(0,x.jsxs)(`p`,{children:[n,`:`,e.consolidation?.settings[t]==null?`未记录`:e.consolidation.settings[t]?`开启`:`关闭`]},t)),(0,x.jsx)(`p`,{className:`fine-print`,children:`设置修改只影响之后接受的新运行。`})]})]}),(0,x.jsx)(Xt,{data:e}),(e.consolidation||e.items.some(e=>e.phase===`consolidation`))&&(0,x.jsxs)(x.Fragment,{children:[(0,x.jsx)(qt,{name:`记忆合并`,items:e.items.filter(e=>e.outcome===`merged`),openMemory:t,runId:e.id,refresh:n}),(0,x.jsx)(qt,{name:`自主处理结果`,items:e.items.filter(e=>e.details.auto_resolution===`auto_v1`&&e.outcome!==`merged`),openMemory:t,runId:e.id,refresh:n}),(0,x.jsx)(qt,{name:`整理建议(模型建议)`,items:e.items.filter(e=>[`conflicts`,`dependencies_reviewed`].includes(e.outcome)),openMemory:t,runId:e.id})]}),i&&(0,x.jsxs)(`section`,{className:`detail-section`,"aria-label":`persona 变化`,children:[(0,x.jsx)(`h3`,{children:`persona 变化`}),(0,x.jsxs)(`p`,{children:[(0,x.jsx)(M,{tone:i.status===`published`?`green`:``,children:{published:`已发布`,pending:`待确认,当前版本保留`,rejected:`已拒绝,当前版本保留`,not_due:`尚未到更新条件`,skipped:`已跳过`,failed:`失败`,conflict:`版本或依据冲突`,running:`更新中`}[i.status]||i.status}),i.change_degree&&` · 变化程度:${R[i.change_degree]||i.change_degree}`]}),i.reason&&(0,x.jsx)(`p`,{children:pe(i.reason)}),i.version_id&&i.base_version_id?(0,x.jsxs)(`a`,{href:`#/persona?tab=history&version=${i.version_id}&before=${i.base_version_id}&after=${i.version_id}`,children:[`对比 persona v`,i.base_version_id,` → v`,i.version_id]}):(0,x.jsx)(`a`,{href:`#/persona?tab=attempts`,children:`查看 persona 生成记录`}),i.content&&(0,x.jsxs)(`details`,{children:[(0,x.jsx)(`summary`,{children:`查看候选正文与检查`}),(0,x.jsx)(`p`,{className:`persona-content`,children:i.content}),i.checks&&(0,x.jsx)(W,{version:{checks:i.checks}})]})]}),(0,x.jsx)(Yt,{data:e}),(0,x.jsx)(Jt,{data:e})]})}function Qt({detail:e,busy:t,blocked:n,setBusy:r,saved:i,failed:a,openMemory:o,close:s}){let[c,u]=(0,l.useState)(null),d=t||n;async function f(){if(c&&!d){r(!0);try{i(await g(`/memories/${e.id}/annotations/${c.annotation.id}${c.kind===`confirm`?`/confirm`:``}`,_(c.kind===`confirm`?`POST`:`DELETE`,{expected_revision:c.revision}))),u(null)}catch(e){a(e)}finally{r(!1)}}}return(0,x.jsxs)(`section`,{className:`detail-section`,"aria-label":`整理建议`,children:[(0,x.jsxs)(`h3`,{children:[`整理建议 `,(0,x.jsx)(M,{children:`模型建议 · 仅管理员可见`})]}),(0,x.jsx)(`p`,{className:`fine-print`,children:`建议可能有误,请结合来源核对。未确认和已确认的建议都不会进入宿主召回。`}),!e.consolidation_annotations?.length&&(0,x.jsx)(`p`,{className:`quiet`,children:`暂无整理建议。`}),e.consolidation_annotations?.map(n=>(0,x.jsxs)(`article`,{className:`maintenance-item`,children:[(0,x.jsxs)(`div`,{className:`detail-badges`,children:[(0,x.jsx)(M,{tone:`warning`,children:n.report.suggestion_type===`merge_details`?jt:ce[n.kind]||n.kind}),(0,x.jsx)(M,{children:le[n.review_status]}),n.modified_since_annotation&&(0,x.jsx)(M,{tone:`warning`,children:`标注后已修改`}),(0,x.jsxs)(`span`,{className:`muted`,children:[`建议 #`,n.id]})]}),n.report.suggestion_type===`merge_details`&&(0,x.jsx)(Mt,{details:n.report.unique_details,ids:n.report.detail_memory_ids||n.related_memory_ids,openMemory:o,disabled:t}),(0,x.jsx)(`p`,{className:`report-text`,children:n.text}),(0,x.jsxs)(`p`,{className:`muted`,children:[`标注时修订 `,n.memory_revision,` · `,(0,x.jsx)(N,{value:n.created_at})]}),n.confirmed_at&&(0,x.jsxs)(`p`,{children:[`管理员已确认 · `,(0,x.jsx)(N,{value:n.confirmed_at})]}),(0,x.jsxs)(`div`,{className:`actions`,children:[n.related_memory_ids.filter(t=>t!==e.id).map(e=>(0,x.jsxs)(`button`,{className:`text-button`,disabled:t,onClick:()=>o(e),children:[`相关记忆 #`,e]},e)),n.superseded_by!=null&&(0,x.jsxs)(`button`,{className:`text-button`,disabled:t,onClick:()=>o(n.superseded_by),children:[`建议的替代记忆 #`,n.superseded_by]})]}),n.report.reason&&(0,x.jsx)(`p`,{className:`report-text`,children:n.report.reason}),(0,x.jsx)(Wt,{value:n.report.source_excerpts,openMemory:o}),(0,x.jsxs)(`a`,{href:`#/status?run=${n.report.run_id}`,onClick:e=>{t?e.preventDefault():s()},children:[`查看整理报告 #`,n.report.run_id]}),(0,x.jsxs)(`div`,{className:`actions suggestion-actions`,children:[n.review_status===`pending`&&(0,x.jsxs)(`button`,{disabled:d||!!c,onClick:()=>u({kind:`confirm`,annotation:n,revision:e.revision}),children:[`采纳建议 #`,n.id]}),(0,x.jsxs)(`button`,{disabled:d||!!c,onClick:()=>u({kind:`clear`,annotation:n,revision:e.revision}),children:[`清除建议 #`,n.id]})]}),c?.annotation.id===n.id&&(0,x.jsxs)(`div`,{className:`delete-confirm`,role:`group`,"aria-label":`确认建议操作`,children:[(0,x.jsx)(`p`,{children:c.kind===`confirm`?n.report.suggestion_type===`merge_details`?`采纳只记录管理员确认,不执行合并,不改正文和相信程度;建议仍只对管理员可见。`:`采纳只记录管理员确认,不改正文和相信程度;建议仍只对管理员可见。`:`清除会撤下这条整理建议并保留操作记录,不改正文和相信程度。`}),(0,x.jsxs)(`div`,{className:`actions`,children:[(0,x.jsx)(`button`,{className:`primary`,disabled:d,onClick:()=>void f(),children:c.kind===`confirm`?`确认采纳建议`:`确认清除建议`}),(0,x.jsx)(`button`,{disabled:t,onClick:()=>u(null),children:`取消建议操作`})]})]})]},n.id))]})}function $t({notice:e}){return e?(0,x.jsx)(A,{children:e.message}):null}var en={memory_source:`仍是其他记忆的来源`,batch_segment:`等待或运行中的批次仍在使用`,goal_source:`仍是目标的来源`,subject_alias:`仍是主体别名的证据`,subject_link:`仍是主体联系的证据`,learning_request:`立即学习请求仍在使用`};function tn({result:e}){return(0,x.jsxs)(`div`,{className:`lifecycle-stack`,children:[(0,x.jsxs)(A,{children:[`已彻底清除记忆 #`,e.memory_id]}),(0,x.jsx)($t,{notice:e.backup_notice}),(0,x.jsxs)(`section`,{children:[(0,x.jsx)(`h3`,{children:`已删除的来源消息`}),e.deleted_message_ids.length?(0,x.jsx)(`ul`,{children:e.deleted_message_ids.map(e=>(0,x.jsxs)(`li`,{children:[`消息 #`,e,` · 没有其他受保护的引用`]},e))}):(0,x.jsx)(`p`,{className:`quiet`,children:`没有删除来源消息。`})]}),(0,x.jsxs)(`section`,{children:[(0,x.jsx)(`h3`,{children:`被保留的来源消息及原因`}),e.retained_messages.length?(0,x.jsx)(`ul`,{children:e.retained_messages.map(e=>(0,x.jsxs)(`li`,{children:[`消息 #`,e.message_id,` ·`,` `,e.reasons.map(e=>en[e]||e).join(`;`)]},e.message_id))}):(0,x.jsx)(`p`,{className:`quiet`,children:`没有被保留的来源消息。`})]})]})}function nn({detail:e,busy:t,blocked:n,setBusy:r,saved:i,failed:a,purged:o,recreated:s}){let[c,u]=(0,l.useState)(null),[d,f]=(0,l.useState)(!1),[p,m]=(0,l.useState)(``),[h,v]=(0,l.useState)(``),[y,b]=(0,l.useState)(``),[S,C]=(0,l.useState)(``),w=t||n,T=new Map;for(let t of e.revisions)T.set(t.revision_after,t.after);for(let t of e.revisions)T.set(t.revision_before,t.before);let E=T.get(Number(S));async function D(t,n={}){if(!w){r(!0);try{let r=`/memories/${e.id}/${t}`,a=_(t===`lifecycle`?`PATCH`:`POST`,{expected_revision:e.revision,...n});t===`purge`?o(await g(r,a)):t===`recreate`?s((await g(r,a)).id):i(await g(r,a)),u(null),f(!1)}catch(e){a(e)}finally{r(!1)}}}function O(t){if(t.preventDefault(),b(``),[p,h].some(e=>!e.trim()||!Number.isInteger(Number(e))||Number(e)<0||Number(e)>100)){b(`重要度和保留强度须为 0—100 的整数`);return}let n={};if(Number(p)!==e.importance&&(n.importance=Number(p)),Number(h)!==e.retention&&(n.retention=Number(h)),!Object.keys(n).length){u(null);return}D(`lifecycle`,n)}let k=()=>{u(null),f(!1),b(``)};return(0,x.jsxs)(`section`,{className:`detail-section lifecycle-stack`,"aria-label":`生命周期操作`,children:[(0,x.jsx)(`h3`,{children:`生命周期操作`}),!c&&(0,x.jsxs)(`div`,{className:`actions`,children:[e.lifecycle!==`deleted`&&(0,x.jsxs)(x.Fragment,{children:[(0,x.jsx)(`button`,{className:`secondary`,disabled:w,onClick:()=>void D(`lifecycle`,{pinned:!e.pinned}),children:e.pinned?`取消置顶`:`置顶记忆`}),e.lifecycle===`forgotten`?(0,x.jsx)(`button`,{className:`secondary`,disabled:w,onClick:()=>void D(`restore`),children:`恢复记忆`}):(0,x.jsx)(`button`,{className:`secondary`,disabled:w,onClick:()=>u(`forget`),children:`手动遗忘`}),(0,x.jsx)(`button`,{className:`secondary`,disabled:w,onClick:()=>{u(`scores`),m(String(e.importance)),v(String(e.retention))},children:`调整分数`})]}),(0,x.jsx)(`button`,{className:`secondary`,disabled:w||!T.size,onClick:()=>{C(``),u(`recreate`)},children:`按旧内容新建`}),(0,x.jsx)(`button`,{className:`text-button danger-text`,disabled:w,onClick:()=>u(`purge`),children:`彻底清除`})]}),!T.size&&(0,x.jsx)(`p`,{className:`fine-print`,children:`尚无历史修订可用于“按旧内容新建”。`}),!!e.pinned&&e.lifecycle!==`deleted`&&(0,x.jsx)(`p`,{className:`lifecycle-help`,children:`置顶记忆不自动衰减、遗忘、扣减或删除。置顶不会自动恢复已遗忘的记忆。`}),c===`scores`&&(0,x.jsx)(`form`,{onSubmit:O,noValidate:!0,children:(0,x.jsxs)(`fieldset`,{className:`lifecycle-fieldset`,disabled:w,children:[(0,x.jsxs)(`div`,{className:`form-grid`,children:[(0,x.jsxs)(`label`,{children:[`调整重要度`,(0,x.jsx)(`input`,{type:`number`,min:0,max:100,step:1,required:!0,value:p,onChange:e=>m(e.target.value)})]}),(0,x.jsxs)(`label`,{children:[`调整保留强度`,(0,x.jsx)(`input`,{type:`number`,min:0,max:100,step:1,required:!0,value:h,onChange:e=>v(e.target.value)})]})]}),(0,x.jsx)(`p`,{className:`lifecycle-help`,children:`保留强度调整会按当前阈值判断遗忘或恢复。`}),y&&(0,x.jsx)(A,{error:!0,children:y}),(0,x.jsxs)(`div`,{className:`actions`,children:[(0,x.jsx)(`button`,{className:`primary`,children:`保存分数`}),(0,x.jsx)(`button`,{className:`secondary`,type:`button`,onClick:k,children:`取消调整`})]})]})}),c===`forget`&&(0,x.jsxs)(`div`,{className:`delete-confirm`,children:[(0,x.jsx)(`p`,{children:`确认手动遗忘?保留强度会降至遗忘阈值以下,并取消置顶;此后只参与深度召回。恢复时不会重新置顶。`}),(0,x.jsxs)(`div`,{className:`actions`,children:[(0,x.jsx)(`button`,{className:`primary`,disabled:w,onClick:()=>void D(`forget`),children:`确认遗忘`}),(0,x.jsx)(`button`,{className:`secondary`,disabled:t,onClick:k,children:`取消遗忘`})]})]}),c===`purge`&&(0,x.jsxs)(`div`,{className:`purge-confirm`,children:[(0,x.jsx)(`h3`,{children:`彻底清除不可撤销`}),(0,x.jsx)(`p`,{children:`将删除记忆正文、修订历史,以及只被这条记忆引用且没有其他受保护引用的来源消息。其他引用仍需要的消息会保留,执行后显示消息标识与原因。`}),(0,x.jsx)(`p`,{children:`同时清空可关联的模型原始输出等正文副本。`}),(0,x.jsx)($t,{notice:e.purge_preview?.backup_notice}),(0,x.jsxs)(`label`,{className:`check-label`,children:[(0,x.jsx)(`input`,{type:`checkbox`,checked:d,disabled:w,onChange:e=>f(e.target.checked)}),`我理解此操作不可撤销`]}),(0,x.jsxs)(`div`,{className:`actions`,children:[(0,x.jsx)(`button`,{className:`danger-button`,disabled:w||!d,onClick:()=>void D(`purge`,{confirm:!0}),children:`再次确认并彻底清除`}),(0,x.jsx)(`button`,{className:`secondary`,disabled:t,onClick:k,children:`取消清除`})]})]}),c===`recreate`&&(0,x.jsxs)(`div`,{className:`lifecycle-stack`,children:[(0,x.jsxs)(`label`,{children:[`选择历史修订`,(0,x.jsxs)(`select`,{disabled:w,value:S,onChange:e=>C(e.target.value),children:[(0,x.jsx)(`option`,{value:``,children:`请选择修订`}),[...T.keys()].sort((e,t)=>t-e).map(e=>(0,x.jsxs)(`option`,{value:e,children:[`修订 `,e]},e))]})]}),E&&(0,x.jsxs)(`div`,{className:`revision`,"data-testid":`revision-preview`,children:[(0,x.jsx)(`p`,{children:String(E.content??``)}),(0,x.jsxs)(`small`,{children:[`相信程度 `,String(E.belief??e.belief),` · 重要度`,` `,String(E.importance??e.importance)]})]}),(0,x.jsx)(`p`,{className:`lifecycle-help`,children:`新建使用新标识,原记忆保持原状。正文和判断取所选修订,涉及人物、标签和来源沿用当前保存的关系;不继承置顶、遗忘时间或向量。`}),(0,x.jsxs)(`div`,{className:`actions`,children:[(0,x.jsx)(`button`,{className:`primary`,disabled:w||!E,onClick:()=>void D(`recreate`,{source_revision:Number(S)}),children:`确认新建`}),(0,x.jsx)(`button`,{className:`secondary`,disabled:t,onClick:k,children:`取消新建`})]})]})]})}function rn({version:e,openMemory:t,onChange:n}){let[r,i]=(0,l.useState)(0),a=y(`/memories/upcoming-deletion?limit=30&offset=${r}`),[o,s]=(0,l.useState)(null),[c,u]=(0,l.useState)(``),[d,p]=(0,l.useState)(``),[m,h]=(0,l.useState)(!1);(0,l.useEffect)(()=>{a.refresh()},[e,a.refresh]),(0,l.useEffect)(()=>{a.data&&r&&r>=a.data.total&&i(Math.max(0,Math.ceil(a.data.total/30)*30-30))},[a.data,r]);async function b(e){if(!(o!==null||m||a.loading||a.error)){s(e.id),u(``),p(``);try{await g(`/memories/${e.id}/restore`,_(`POST`,{expected_revision:e.revision})),p(`已恢复记忆 #${e.id}`),a.refresh(),n()}catch(e){e instanceof f&&(e.status===409||e.status===404)?(h(!0),u(`这条记忆已变化或已被删除,请刷新列表后再操作。`)):u(v(e))}finally{s(null)}}}return(0,x.jsxs)(x.Fragment,{children:[(0,x.jsxs)(`div`,{className:`panel-heading`,children:[(0,x.jsx)(`h2`,{children:`即将删除`}),(0,x.jsx)(`button`,{className:`secondary`,disabled:o!==null,onClick:()=>{h(!1),u(``),a.refresh()},children:`刷新列表`})]}),(0,x.jsx)(`p`,{className:`learning-intro`,children:`包括遗忘期将满和已到期等待维护的记忆;置顶记忆不自动删除。到期后由维护执行删除,可以在此提前恢复。`}),(c||a.error)&&(0,x.jsx)(A,{error:!0,children:c||a.error}),d&&(0,x.jsx)(A,{children:d}),!a.data&&!a.error&&(0,x.jsx)(`p`,{role:`status`,children:`正在读取即将删除的记忆…`}),a.data&&!a.data.enabled&&(0,x.jsxs)(A,{children:[`自动删除已关闭,当前没有即将删除的记忆。`,(0,x.jsx)(`a`,{href:`#/settings`,children:`前往生命周期设置`})]}),a.data?.enabled&&!a.data.items.length&&(0,x.jsx)(j,{title:`暂无即将删除的记忆`}),(0,x.jsx)(`div`,{className:`memory-grid`,children:a.data?.items.map(e=>(0,x.jsxs)(`article`,{className:`panel upcoming-card`,children:[(0,x.jsxs)(`div`,{className:`memory-card-top`,children:[(0,x.jsx)(M,{children:`已遗忘`}),(0,x.jsxs)(`span`,{className:`muted`,children:[`#`,e.id,` · 保留强度 `,e.retention]})]}),(0,x.jsx)(`button`,{className:`memory-preview`,onClick:()=>t(e.id),children:e.content}),(0,x.jsxs)(`p`,{children:[`遗忘时间:`,w(e.forgotten_at)]}),(0,x.jsxs)(`p`,{children:[`删除到期时间:`,w(e.delete_after)]}),(0,x.jsx)(`button`,{className:`secondary`,"aria-label":`恢复记忆 #${e.id}`,disabled:o!==null||m||a.loading||!!a.error,onClick:()=>void b(e),children:o===e.id?`正在恢复…`:`恢复记忆`})]},e.id))}),a.data&&(0,x.jsx)(T,{total:a.data.total,offset:r,change:i})]})}var an=[`事件`,`事实`,`偏好`,`关系`,`观点`,`计划`,`自我`,`其他`];function on({openMemory:e,version:t,onChange:n,initialQuery:r}){let[i,a]=(0,l.useState)(`all`);return(0,x.jsxs)(x.Fragment,{children:[(0,x.jsx)(`div`,{className:`page-heading`,children:(0,x.jsxs)(`div`,{children:[(0,x.jsx)(`p`,{className:`eyebrow`,children:`留下的经历,有迹可循`}),(0,x.jsx)(`h1`,{children:`记忆`}),(0,x.jsx)(`p`,{children:`查看内容、追溯来源,管理记忆的保留与遗忘。`})]})}),(0,x.jsxs)(`div`,{className:`learning-tabs`,"aria-label":`记忆视图`,children:[(0,x.jsx)(`button`,{"aria-pressed":i===`all`,onClick:()=>a(`all`),children:`记忆列表`}),(0,x.jsx)(`button`,{"aria-pressed":i===`upcoming`,onClick:()=>a(`upcoming`),children:`即将删除`})]}),i===`all`?(0,x.jsx)(sn,{openMemory:e,version:t,initialQuery:r}):(0,x.jsx)(rn,{openMemory:e,version:t,onChange:n})]})}function sn({openMemory:e,version:t,initialQuery:n=``}){let r=y(`/catalog`),i=new URLSearchParams(n),[a,o]=(0,l.useState)({text:``,person_id:i.get(`person_id`)||``,kind:``,entry_id:``,time_from:``,time_to:``,lifecycle:i.get(`lifecycle`)===`all`?`all`:`active`,pinned:``,sort:`time`}),[s,c]=(0,l.useState)(()=>new URLSearchParams(Object.entries(a).filter(([,e])=>!!e)).toString()),[u,d]=(0,l.useState)(0),[f,p]=(0,l.useState)(``),m=y(`/memories?${s}&limit=30&offset=${u}`);(0,l.useEffect)(()=>{m.refresh()},[t,m.refresh]),(0,l.useEffect)(()=>{m.data&&u&&u>=m.data.total&&d(Math.max(0,Math.ceil(m.data.total/30)*30-30))},[m.data,u]);function h(e){if(e.preventDefault(),a.time_from&&a.time_to&&a.time_from>a.time_to){p(`开始日期不能晚于结束日期`);return}p(``),d(0),c(new URLSearchParams(Object.entries(a).filter(([,e])=>!!e)).toString()),m.refresh()}let g=(e,t)=>o({...a,[e]:t});return(0,x.jsxs)(x.Fragment,{children:[(0,x.jsx)(`section`,{className:`panel`,children:(0,x.jsxs)(`form`,{className:`memory-filters`,onSubmit:h,children:[(0,x.jsxs)(`div`,{className:`search-row`,children:[(0,x.jsxs)(`label`,{className:`search-field`,children:[`搜索记忆`,(0,x.jsx)(`input`,{value:a.text,maxLength:1e3,placeholder:`输入正文或标签中的词…`,onChange:e=>g(`text`,e.target.value)})]}),(0,x.jsx)(`button`,{className:`primary`,type:`submit`,children:`搜索`})]}),(0,x.jsxs)(`div`,{className:`filter-grid`,children:[(0,x.jsxs)(`label`,{children:[`涉及的人`,(0,x.jsxs)(`select`,{value:a.person_id,onChange:e=>g(`person_id`,e.target.value),children:[(0,x.jsx)(`option`,{value:``,children:`全部人物`}),r.data?.people.map(e=>(0,x.jsxs)(`option`,{value:e.id,children:[e.name,(r.data?.people.filter(t=>t.name===e.name).length||0)>1?` · ${e.id.slice(0,6)}`:``]},e.id))]})]}),(0,x.jsxs)(`label`,{children:[`记忆类型`,(0,x.jsxs)(`select`,{value:a.kind,onChange:e=>g(`kind`,e.target.value),children:[(0,x.jsx)(`option`,{value:``,children:`全部类型`}),an.map(e=>(0,x.jsx)(`option`,{children:e},e))]})]}),(0,x.jsxs)(`label`,{children:[`出处入口`,(0,x.jsxs)(`select`,{value:a.entry_id,onChange:e=>g(`entry_id`,e.target.value),children:[(0,x.jsx)(`option`,{value:``,children:`全部入口`}),r.data?.entries.map(e=>(0,x.jsx)(`option`,{value:e.id,children:e.name},e.id))]})]}),(0,x.jsxs)(`label`,{children:[`状态`,(0,x.jsxs)(`select`,{value:a.lifecycle,onChange:e=>g(`lifecycle`,e.target.value),children:[(0,x.jsx)(`option`,{value:`active`,children:`有效`}),(0,x.jsx)(`option`,{value:`forgotten`,children:`已遗忘`}),(0,x.jsx)(`option`,{value:`deleted`,children:`已删除(历史)`}),(0,x.jsx)(`option`,{value:`all`,children:`全部状态`})]})]}),(0,x.jsxs)(`label`,{children:[`置顶筛选`,(0,x.jsxs)(`select`,{"aria-label":`置顶筛选`,value:a.pinned,onChange:e=>g(`pinned`,e.target.value),children:[(0,x.jsx)(`option`,{value:``,children:`全部`}),(0,x.jsx)(`option`,{value:`true`,children:`已置顶`}),(0,x.jsx)(`option`,{value:`false`,children:`未置顶`})]})]}),(0,x.jsxs)(`label`,{children:[`开始日期`,(0,x.jsx)(`input`,{type:`date`,value:a.time_from,onChange:e=>g(`time_from`,e.target.value)})]}),(0,x.jsxs)(`label`,{children:[`结束日期`,(0,x.jsx)(`input`,{type:`date`,value:a.time_to,onChange:e=>g(`time_to`,e.target.value)})]}),(0,x.jsxs)(`label`,{children:[`排序方式`,(0,x.jsxs)(`select`,{value:a.sort,onChange:e=>g(`sort`,e.target.value),children:[(0,x.jsx)(`option`,{value:`time`,children:`更新时间 · 从新到旧`}),(0,x.jsx)(`option`,{value:`relevance`,children:`文本相关度`}),(0,x.jsx)(`option`,{value:`retention`,children:`保留强度 · 从高到低`})]})]})]}),(0,x.jsx)(`p`,{className:`fine-print`,children:`日期按事件时间筛选;没有事件时间时使用创建时间。`})]})}),f&&(0,x.jsx)(A,{error:!0,children:f}),(m.error||r.error)&&(0,x.jsx)(A,{error:!0,children:m.error||r.error}),(0,x.jsxs)(`div`,{className:`list-heading`,children:[(0,x.jsx)(`span`,{children:m.data?`共 ${m.data.total} 条`:`正在读取…`}),(0,x.jsx)(`span`,{children:`浏览列表不会增加召回或使用次数`})]}),m.data?.items.length===0&&(0,x.jsx)(`section`,{className:`panel`,children:(0,x.jsx)(j,{title:`没有找到记忆`,children:`尝试换一个词,或调整筛选条件。`})}),(0,x.jsx)(`div`,{className:`memory-grid`,children:m.data?.items.map(t=>(0,x.jsxs)(`button`,{className:`memory-card panel`,onClick:()=>e(t.id),children:[(0,x.jsxs)(`div`,{className:`memory-card-top`,children:[(0,x.jsx)(M,{tone:`purple`,children:t.kind}),(0,x.jsx)(M,{tone:t.pinned?`purple`:``,children:t.pinned?`已置顶`:`未置顶`}),(0,x.jsxs)(`span`,{className:`muted`,children:[`#`,t.id,` · `,E(t.lifecycle)]})]}),(0,x.jsx)(`p`,{className:`memory-content`,children:t.content}),(0,x.jsx)(`div`,{className:`person-line`,children:t.about.map(e=>e.name).join(`、`)||`未标注涉及人物`}),(0,x.jsxs)(`div`,{className:`memory-card-bottom`,children:[(0,x.jsxs)(`span`,{children:[`相信 `,t.belief,` · 保留强度 `,t.retention]}),(0,x.jsx)(`time`,{children:S(t.updated_at)}),(0,x.jsx)(`span`,{"aria-hidden":!0,children:`↗`})]})]},t.id))}),m.data&&(0,x.jsx)(T,{total:m.data.total,offset:u,change:d})]})}function cn({id:e,onClose:t,onChange:n,openMemory:r}){let i=y(`/memories/${e}`),a=y(`/catalog`),[o,s]=(0,l.useState)(null),[c,u]=(0,l.useState)(!1),[d,p]=(0,l.useState)(``),[m,h]=(0,l.useState)(!1),[b,C]=(0,l.useState)(``),[T,D]=(0,l.useState)(!1),[O,k]=(0,l.useState)(!1),[j,N]=(0,l.useState)(null),[P,F]=(0,l.useState)(0),I=j?null:o||i.data,te=e=>{O||r(e)};function L(e){e instanceof f&&e.code===`revision_conflict`?(D(!0),C(`其他操作已更新了这条记忆。草稿暂时保留;请刷新并载入最新修订,再核对后操作。载入会替换当前草稿。`)):e instanceof f&&e.status===404?(D(!0),C(`这条记忆已删除或已彻底清除,请刷新或返回列表查看。`)):C(v(e))}async function ne(t){if(t.preventDefault(),!(!I||O||T)){k(!0),C(``);try{s(await g(`/memories/${e}`,_(`PATCH`,{expected_revision:I.revision,content:d}))),u(!1),D(!1),n()}catch(e){L(e)}finally{k(!1)}}}async function R(){if(!(!I||O||T)){k(!0),C(``);try{s(await g(`/memories/${e}`,_(`DELETE`,{expected_revision:I.revision}))),h(!1),F(e=>e+1),u(!1),n()}catch(e){L(e)}finally{k(!1)}}}async function re(){k(!0);try{let t=await g(`/memories/${e}`);s(t),p(t.content),F(e=>e+1),D(!1),C(``),h(!1)}catch(e){L(e)}finally{k(!1)}}return(0,x.jsxs)(ee,{title:`记忆 #${e}`,onClose:t,closeDisabled:O,children:[(b||i.error)&&(0,x.jsx)(A,{error:!0,children:b||i.error}),T&&(0,x.jsx)(`button`,{className:`secondary`,disabled:O,onClick:re,children:`载入最新修订`}),j&&(0,x.jsx)(tn,{result:j}),!I&&!j&&!i.error&&(0,x.jsx)(`p`,{className:`quiet`,children:`正在读取详情…`}),I&&(0,x.jsxs)(`div`,{className:`memory-detail`,children:[(0,x.jsxs)(`div`,{className:`detail-badges`,children:[(0,x.jsx)(M,{tone:`purple`,children:I.kind}),(0,x.jsx)(M,{children:I.stance}),(0,x.jsx)(M,{tone:I.lifecycle===`deleted`?`danger`:``,children:E(I.lifecycle)}),(0,x.jsx)(M,{tone:I.pinned?`purple`:``,children:I.pinned?`已置顶`:`未置顶`}),(0,x.jsxs)(`span`,{className:`muted`,children:[`修订 `,I.revision]})]}),I.merged_into!=null&&(0,x.jsxs)(A,{children:[(0,x.jsxs)(`button`,{className:`text-button`,disabled:O,onClick:()=>te(I.merged_into),children:[`已合并到 #`,I.merged_into]}),(0,x.jsx)(`p`,{children:`全部来源已归入保留记忆;此处是原记忆占位,不能恢复为有效记忆。`})]}),I.lifecycle===`deleted`&&I.merged_into==null&&(0,x.jsx)(A,{children:`此标识已撤销,不再参与召回;来源和修订历史仍然保留。`}),c&&I.lifecycle!==`deleted`?(0,x.jsxs)(`form`,{onSubmit:ne,children:[(0,x.jsxs)(`label`,{children:[`记忆正文`,(0,x.jsx)(`textarea`,{value:d,maxLength:1e3,rows:5,onChange:e=>p(e.target.value)})]}),(0,x.jsxs)(`div`,{className:`actions`,children:[(0,x.jsx)(`button`,{className:`primary`,disabled:O||T||!d.trim(),children:`保存修改`}),(0,x.jsx)(`button`,{type:`button`,className:`secondary`,disabled:O,onClick:()=>u(!1),children:`取消编辑`}),(0,x.jsxs)(`small`,{children:[d.length,` / 1000 字`]})]})]}):(0,x.jsx)(`p`,{className:`detail-content`,children:I.content}),(0,x.jsxs)(`div`,{className:`score-grid`,children:[(0,x.jsxs)(`div`,{children:[(0,x.jsx)(`strong`,{children:I.belief}),(0,x.jsx)(`span`,{children:`相信程度`})]}),(0,x.jsxs)(`div`,{children:[(0,x.jsx)(`strong`,{children:I.importance}),(0,x.jsx)(`span`,{children:`重要度`})]}),(0,x.jsxs)(`div`,{children:[(0,x.jsx)(`strong`,{children:I.retention}),(0,x.jsx)(`span`,{children:`保留强度`})]})]}),(0,x.jsx)(nn,{detail:I,busy:O,blocked:T||c||m,setBusy:k,failed:L,saved:e=>{s(e),C(``),n()},purged:e=>{N(e),s(null),C(``),n()},recreated:e=>{n(),r(e)}},`${I.id}:${I.revision}:${P}`),(0,x.jsxs)(`dl`,{className:`metadata`,children:[(0,x.jsxs)(`div`,{children:[(0,x.jsx)(`dt`,{children:`记忆可见范围`}),(0,x.jsx)(`dd`,{children:nt(I.visibility,a.data?.entries||[])})]}),I.lifecycle===`forgotten`&&(0,x.jsxs)(`div`,{children:[(0,x.jsx)(`dt`,{children:`遗忘时间`}),(0,x.jsx)(`dd`,{children:w(I.forgotten_at)})]}),(0,x.jsxs)(`div`,{children:[(0,x.jsx)(`dt`,{children:`说话人`}),(0,x.jsx)(`dd`,{children:I.speaker.name})]}),(0,x.jsxs)(`div`,{children:[(0,x.jsx)(`dt`,{children:`涉及的人`}),(0,x.jsx)(`dd`,{children:I.about.map(e=>e.name).join(`、`)||`未标注`})]}),(0,x.jsxs)(`div`,{children:[(0,x.jsx)(`dt`,{children:`事件时间`}),(0,x.jsx)(`dd`,{children:I.event_time||`未标注`})]}),(0,x.jsxs)(`div`,{children:[(0,x.jsx)(`dt`,{children:`更新时间`}),(0,x.jsx)(`dd`,{children:S(I.updated_at)})]}),(0,x.jsxs)(`div`,{children:[(0,x.jsx)(`dt`,{children:`被召回`}),(0,x.jsxs)(`dd`,{children:[I.recall_count,` 次`]})]}),(0,x.jsxs)(`div`,{children:[(0,x.jsx)(`dt`,{children:`被使用`}),(0,x.jsxs)(`dd`,{children:[I.used_count,` 次`]})]})]}),(0,x.jsx)(`p`,{className:`fine-print`,children:`被召回不等于被使用;使用次数不代表内容可信度。`}),!!I.tags.length&&(0,x.jsx)(`div`,{className:`tag-list`,children:I.tags.map(e=>(0,x.jsxs)(M,{children:[`# `,e]},e))}),(0,x.jsx)(Qt,{detail:I,busy:O,blocked:T||c||m,setBusy:k,saved:e=>{s(e),C(``),n()},failed:L,openMemory:te,close:t},`suggestions-${I.id}:${I.revision}:${P}`),(0,x.jsxs)(`section`,{className:`detail-section`,children:[(0,x.jsx)(`h3`,{children:`来源与前后文`}),I.sources.length?I.sources.map(e=>(0,x.jsx)(ln,{source:e,openMemory:te},e.id)):(0,x.jsx)(`p`,{className:`quiet`,children:`没有保存的来源消息。`})]}),(0,x.jsxs)(`section`,{className:`detail-section`,children:[(0,x.jsx)(`h3`,{children:`基于它形成的记忆`}),I.derived_memories.length?I.derived_memories.map(e=>(0,x.jsxs)(`button`,{className:`memory-preview`,disabled:O,onClick:()=>te(e.id),children:[(0,x.jsxs)(`span`,{children:[`#`,e.id,` · `,e.content]}),(0,x.jsxs)(`small`,{children:[E(e.lifecycle),e.needs_review?` · 来源已变化,待复核`:``]})]},e.id)):(0,x.jsx)(`p`,{className:`quiet`,children:`暂无派生记忆。`})]}),(0,x.jsxs)(`details`,{className:`history`,children:[(0,x.jsxs)(`summary`,{children:[`修订历史 `,(0,x.jsx)(M,{children:I.revisions.length})]}),I.revisions.length?I.revisions.map(e=>(0,x.jsx)(un,{revision:e},e.id)):(0,x.jsx)(`p`,{className:`quiet`,children:`还没有修订记录。`})]}),(0,x.jsx)(`a`,{className:`operation-link`,href:`#/operations?object_type=memory&object_id=${e}`,onClick:e=>{O?e.preventDefault():t()},children:`查看此记忆的操作记录`}),I.lifecycle!==`deleted`&&(0,x.jsx)(`div`,{className:`detail-footer`,children:m?(0,x.jsxs)(`div`,{className:`delete-confirm`,children:[(0,x.jsx)(`p`,{children:`确认删除这条记忆?此标识将永久撤销,共享的来源消息与其他记忆保留。`}),(0,x.jsxs)(`div`,{className:`actions`,children:[(0,x.jsx)(`button`,{className:`danger-button`,disabled:O||T,onClick:R,children:`确认删除`}),(0,x.jsx)(`button`,{className:`secondary`,disabled:O,onClick:()=>h(!1),children:`取消`})]})]}):(0,x.jsxs)(`div`,{className:`actions`,children:[(0,x.jsx)(`button`,{className:`secondary`,disabled:O||T||c,onClick:()=>{u(!0),p(I.content),C(``)},children:`编辑正文`}),(0,x.jsx)(`button`,{className:`text-button danger-text`,disabled:O||T||c,onClick:()=>h(!0),children:`删除记忆`})]})})]})]})}function ln({source:e,openMemory:t,initiallyOpen:n=!1,speakerNames:r={}}){return(0,x.jsxs)(`details`,{className:`source`,open:n||void 0,children:[(0,x.jsx)(`summary`,{children:e.kind===`message`?`来源消息 · ${e.message?.sender_name||`原文已不可用`}`:e.kind===`memory`?`来源记忆 #${e.memory?.id||``}`:`初始背景`}),e.message&&(0,x.jsxs)(x.Fragment,{children:[(0,x.jsxs)(`p`,{className:`muted`,children:[e.message.entry_name||e.message.entry_id,` ·`,` `,S(e.message.occurred_at)]}),(0,x.jsx)(`blockquote`,{children:e.message.content}),e.message.quote_content&&(0,x.jsxs)(`blockquote`,{children:[`转引原话(原作者:`,r[e.message.quote_author_subject_id||``]||e.message.quote_author_subject_id||`未记录`,`):`,e.message.quote_content]}),(0,x.jsx)(qe,{media:e.message.media}),(0,x.jsxs)(`details`,{className:`source-context`,children:[(0,x.jsx)(`summary`,{children:`查看前后文`}),e.context?.map(t=>(0,x.jsxs)(`div`,{className:`context-message ${t.id===e.message_id?`highlight`:``}`,children:[(0,x.jsxs)(`small`,{children:[t.sender_name,` · `,S(t.occurred_at)]}),(0,x.jsx)(`p`,{children:t.content}),(0,x.jsx)(qe,{media:t.media})]},t.id))]})]}),e.memory&&(0,x.jsxs)(x.Fragment,{children:[(0,x.jsx)(`button`,{className:`memory-preview`,onClick:()=>t(e.memory.id),children:e.memory.content}),(0,x.jsxs)(`p`,{className:`fine-print`,children:[`依据修订 `,e.source_revision,` · `,E(e.memory.lifecycle),e.needs_review?` · 来源已变化,待复核`:``]})]}),e.kind===`initial_setting`&&(0,x.jsx)(`p`,{children:e.note||`角色初始背景材料`})]})}function un({revision:e}){return(0,x.jsxs)(`article`,{className:`revision`,children:[(0,x.jsxs)(`small`,{children:[S(e.created_at),` ·`,` `,{admin:`管理员`,learning:`学习`,maintenance:`维护`,entry_profile:`画像学习`}[e.actor]||e.actor,` `,`· 修订 `,e.revision_before,` → `,e.revision_after]}),(0,x.jsxs)(`p`,{children:[(0,x.jsx)(`span`,{className:`muted`,children:`修改前`}),` `,String(e.before.content||``)]}),(0,x.jsxs)(`p`,{className:`muted`,children:[`相信程度 `,String(e.before.belief??`—`),` →`,` `,String(e.after.belief??`—`),` · 重要度`,` `,String(e.before.importance??`—`),` →`,` `,String(e.after.importance??`—`)]}),(0,x.jsxs)(`p`,{children:[(0,x.jsx)(`span`,{className:`muted`,children:`修改后`}),` `,e.after.lifecycle===`deleted`?`已删除(撤销对象)`:String(e.after.content||``)]})]})}function dn({goal:e,openMemory:t}){let[n,r]=(0,l.useState)(0),i=y(`/goals/${e.id}/sources?limit=30&offset=${n}`);return(0,x.jsxs)(`section`,{className:`panel goals-panel`,"aria-label":`来源消息`,children:[(0,x.jsxs)(`div`,{className:`panel-heading`,children:[(0,x.jsx)(`h2`,{children:`来源消息`}),(0,x.jsx)(`button`,{className:`text-button`,onClick:i.refresh,children:`刷新来源消息`})]}),(0,x.jsx)(`p`,{className:`goal-help`,children:`前后文来自同一入口的相邻消息,只读查看不会成为新经历。`}),i.error&&(0,x.jsxs)(A,{error:!0,children:[i.error,(0,x.jsx)(`button`,{onClick:i.refresh,children:`重试来源消息`})]}),!i.data&&!i.error&&(0,x.jsx)(`p`,{role:`status`,children:`正在读取来源消息…`}),i.data?.total===0&&(0,x.jsx)(`p`,{className:`goal-help`,children:`没有关联来源消息。宿主和管理员创建的目标可以没有对话依据。`}),i.data?.items.map(e=>(0,x.jsx)(`div`,{children:e.missing||!e.message?(0,x.jsxs)(`div`,{className:`source`,children:[(0,x.jsxs)(`p`,{children:[`来源消息 #`,e.message_id]}),(0,x.jsx)(`p`,{className:`goal-help`,children:e.notice||`来源消息已清理或不可用`})]}):(0,x.jsx)(ln,{source:{id:e.id,kind:`message`,message_id:e.message_id,message:e.message,context:e.context},openMemory:t})},e.id)),i.data&&(0,x.jsx)(T,{total:i.data.total,offset:n,change:r})]})}var fn={content:`正文`,deadline:`截止时间`,deadline_at:`截止时刻`,reminder_minutes:`提醒提前量`,state:`状态`,kind:`类型`,origin:`来源`,entry_id:`产生入口`,people:`涉及的人`,closed_at:`结束时间`,closed_by:`结束操作者`,merged_into:`合并到目标`,source_message_ids:`来源消息`,promise_memories:`承诺记忆及依据修订`,merged_goal_ids:`合并进来的目标`,duplicates:`可能重复关系`,basis_annotations:`依据复核标注`},pn={create:`创建目标`,update:`更新目标`,merge:`合并目标`,duplicate_dismiss:`驳回重复`,possible_duplicate:`标记可能重复`,basis_clear:`清除依据标注`,basis_review:`复核目标依据`,dedup:`目标去重判断`},mn={"goal created":`创建目标`,"goal updated":`更新目标`,"goals merged":`合并目标`,"possible duplicate identified":`发现可能重复的目标`,"possible duplicate dismissed":`驳回可能重复关系`,"goal basis reviewed":`复核目标依据`,"basis warning cleared by administrator":`管理员清除依据标注`},hn={possible:`可能重复`,dismissed:`已驳回`,merged:`已合并`,active:`生效`,cleared:`已清除`,superseded:`已被新标注替代`,resolved:`依据已恢复或移除`};function gn({field:e,value:t,catalog:n}){return t===void 0?(0,x.jsx)(x.Fragment,{children:`未记录`}):t===null?(0,x.jsx)(x.Fragment,{children:e===`reminder_minutes`?`使用默认值`:`未设置`}):(e===`deadline`||e===`closed_at`)&&typeof t==`string`?(0,x.jsx)(N,{value:t}):e===`reminder_minutes`?(0,x.jsxs)(x.Fragment,{children:[String(t),` 分钟`]}):e===`people`&&Array.isArray(t)?(0,x.jsx)(x.Fragment,{children:t.map(e=>yt(String(e),n)).join(`、`)||`无`}):e===`entry_id`?(0,x.jsx)(x.Fragment,{children:bt(String(t),n)}):e===`state`?(0,x.jsx)(x.Fragment,{children:pt[t]||String(t)}):e===`kind`?(0,x.jsx)(x.Fragment,{children:mt[t]||String(t)}):e===`origin`?(0,x.jsx)(x.Fragment,{children:ht[t]||String(t)}):e===`closed_by`?(0,x.jsx)(x.Fragment,{children:Se[String(t)]||String(t)}):Array.isArray(t)?(0,x.jsx)(x.Fragment,{children:t.map(e=>typeof e==`object`&&e&&`memory_id`in e?`记忆 #${e.memory_id}(依据修订 ${e.memory_revision})`:`#${e}`).join(`、`)||`无`}):typeof t==`object`?(0,x.jsx)(x.Fragment,{children:Object.entries(t).map(([e,t])=>`#${e}:${hn[String(t)]||String(t)}`).join(`;`)||`无`}):(0,x.jsx)(x.Fragment,{children:String(t)})}function _n({id:e,catalog:t}){let[n,r]=(0,l.useState)(0),[i,a]=(0,l.useState)(0),o=y(`/goals/${e}/revisions?limit=30&offset=${n}`),s=y(`/operations?object_type=goal&object_id=${e}&limit=30&offset=${i}`);return(0,x.jsxs)(`section`,{className:`panel goals-panel`,children:[(0,x.jsxs)(`div`,{className:`panel-heading`,children:[(0,x.jsx)(`h2`,{children:`修订与操作记录`}),(0,x.jsx)(`button`,{className:`text-button`,onClick:()=>{o.refresh(),s.refresh()},children:`刷新目标记录`})]}),(0,x.jsxs)(`section`,{"aria-label":`修订历史`,children:[(0,x.jsx)(`h3`,{children:`修订历史`}),(0,x.jsx)(`p`,{className:`goal-help`,children:`每次仅展示发生变化的字段;依据标注和重复关系的变化也会留下修订。`}),o.error&&(0,x.jsxs)(A,{error:!0,children:[o.error,(0,x.jsx)(`button`,{onClick:o.refresh,children:`重试修订历史`})]}),!o.data&&!o.error&&(0,x.jsx)(`p`,{role:`status`,children:`正在读取修订历史…`}),o.data?.history_status===`pre_migration_no_snapshot`&&(0,x.jsxs)(A,{children:[`迁移前无快照:截至修订 `,o.data.missing_through_revision,` `,`的历史值无法还原。`]}),o.data?.items.length===0&&(0,x.jsx)(`p`,{className:`goal-help`,children:`暂无修订快照。`}),o.data?.items.map(e=>(0,x.jsxs)(`article`,{className:`goal-operation`,children:[(0,x.jsxs)(`h4`,{children:[pn[e.action]||xe[`goal_${e.action}`]||e.action,` `,`·`,` `,e.revision_before==null?`新建`:`修订 ${e.revision_before}`,` `,`→ `,e.revision_after]}),(0,x.jsxs)(`p`,{children:[(0,x.jsx)(N,{value:e.created_at}),` ·`,` `,Se[e.actor]||e.actor]}),(0,x.jsx)(`p`,{className:`report-text`,children:mn[e.reason]||e.reason}),(0,x.jsx)(`dl`,{className:`revision-values`,children:Array.from(new Set([...Object.keys(e.before),...Object.keys(e.after)])).map(n=>(0,x.jsxs)(`div`,{children:[(0,x.jsx)(`dt`,{children:fn[n]||n}),(0,x.jsxs)(`dd`,{children:[(0,x.jsx)(`span`,{className:`muted`,children:`修改前`}),(0,x.jsx)(`p`,{children:(0,x.jsx)(gn,{field:n,value:e.before[n],catalog:t})})]}),(0,x.jsxs)(`dd`,{children:[(0,x.jsx)(`span`,{className:`muted`,children:`修改后`}),(0,x.jsx)(`p`,{children:(0,x.jsx)(gn,{field:n,value:e.after[n],catalog:t})})]})]},n))})]},e.id)),o.data&&(0,x.jsx)(T,{total:o.data.total,offset:n,change:r})]}),(0,x.jsxs)(`details`,{className:`goal-operations`,children:[(0,x.jsx)(`summary`,{children:`操作记录`}),s.error&&(0,x.jsx)(A,{error:!0,children:s.error}),!s.data&&!s.error&&(0,x.jsx)(`p`,{role:`status`,children:`正在读取操作记录…`}),s.data?.items.length===0&&(0,x.jsx)(`p`,{className:`goal-help`,children:`暂无操作记录。`}),s.data?.items.map(e=>(0,x.jsxs)(`article`,{className:`goal-operation`,children:[(0,x.jsx)(`h3`,{children:xe[e.action]||e.action}),(0,x.jsxs)(`p`,{children:[(0,x.jsx)(N,{value:e.created_at}),` ·`,` `,Se[e.actor]||e.actor]}),typeof e.details.revision_before==`number`&&(0,x.jsxs)(`p`,{children:[`修改前修订 `,e.details.revision_before]}),Array.isArray(e.details.fields)&&(0,x.jsxs)(`p`,{children:[`修改字段:`,e.details.fields.map(e=>fn[String(e)]||String(e)).join(`、`)]}),typeof e.details.merged_id==`number`&&(0,x.jsxs)(`p`,{children:[`合并进来的目标 #`,e.details.merged_id]}),typeof e.details.other_id==`number`&&(0,x.jsxs)(`p`,{children:[`驳回与目标 #`,e.details.other_id,` 的可能重复关系`]}),e.action===`goal_create`&&(0,x.jsx)(`p`,{children:`已创建目标。`})]},e.id)),s.data&&(0,x.jsx)(T,{total:s.data.total,offset:i,change:a})]}),(0,x.jsx)(`a`,{className:`text-button`,href:`#/operations?object_type=goal&object_id=${e}`,children:`查看完整操作记录`})]})}function vn({goal:e,otherId:t,locked:n,catalog:r,openGoal:i,merge:a,dismiss:o}){let s=y(`/goals/${t}`);return(0,x.jsxs)(`article`,{className:`goal-duplicate`,children:[(0,x.jsxs)(`h3`,{children:[`可能重复的目标 #`,t]}),s.error&&(0,x.jsxs)(A,{error:!0,children:[s.error,(0,x.jsx)(`button`,{onClick:s.refresh,children:`重试读取重复目标`})]}),!s.data&&!s.error&&(0,x.jsx)(`p`,{children:`正在读取…`}),s.data&&(0,x.jsxs)(x.Fragment,{children:[(0,x.jsx)(`p`,{className:`goal-content`,children:s.data.content}),(0,x.jsx)(xt,{goal:s.data}),(0,x.jsx)(Ct,{goal:s.data,catalog:r}),(0,x.jsxs)(`div`,{className:`actions`,children:[(0,x.jsxs)(`button`,{className:`secondary`,onClick:()=>i(t),children:[`查看目标 #`,t]}),(0,x.jsxs)(`button`,{disabled:n||s.loading||!!s.error||e.state!==`open`||s.data.state!==`open`||!!s.data.merged_into,onClick:()=>a(s.data),children:[`合并目标 #`,t]}),(0,x.jsxs)(`button`,{disabled:n||s.loading||!!s.error||!!s.data.merged_into,onClick:()=>o(s.data),children:[`驳回重复 #`,t]})]})]})]})}function yn({id:e,catalog:t,openMemory:n,openGoal:r,onBack:i,onChange:a}){let[o,s]=(0,l.useState)(null),[c,u]=(0,l.useState)(null),[d,p]=(0,l.useState)(!1),[m,h]=(0,l.useState)(!1),[b,S]=(0,l.useState)(``),[C,w]=(0,l.useState)(``),T=y(`/goals/${e}`,o||c||m?0:2e3),D=T.data,O=d||m||T.loading||!!T.error;function k(){s(null),u(null),S(``),w(``),h(!1),T.refresh()}async function j(e,t,n=`PATCH`){if(!(d||m)){p(!0),S(``),w(``);try{let i=await g(e,_(n,t));s(null),u(null),T.refresh(),a(),e.endsWith(`/merge`)?(w(`已合并,以下为保留目标。`),r(i.id)):w(e.endsWith(`/dismiss`)?`已驳回可能重复关系`:`目标已更新`)}catch(e){S(v(e)),e instanceof f&&e.status===409&&(h(!0),u(null))}finally{p(!1)}}}return(0,x.jsxs)(`div`,{className:`goal-detail`,children:[(0,x.jsxs)(`div`,{className:`actions`,children:[(0,x.jsx)(`button`,{className:`text-button`,disabled:d,onClick:i,children:`← 返回目标列表`}),(0,x.jsx)(`button`,{className:`secondary`,disabled:d,onClick:k,children:m?`载入最新目标`:`刷新目标详情`})]}),!c&&(b||T.error)&&(0,x.jsxs)(A,{error:!0,children:[b||T.error,m&&`。请载入最新目标后重新核对;重新加载会放弃当前编辑草稿。`]}),C&&(0,x.jsx)(A,{children:C}),!D&&!T.error&&(0,x.jsx)(`p`,{role:`status`,children:`正在读取目标详情…`}),D&&(0,x.jsxs)(x.Fragment,{children:[(0,x.jsxs)(`section`,{className:`panel goals-panel`,children:[(0,x.jsxs)(`div`,{className:`panel-heading`,children:[(0,x.jsxs)(`h2`,{children:[`目标 #`,D.id]}),(0,x.jsxs)(`span`,{className:`muted`,children:[`修订 `,D.revision]})]}),(0,x.jsx)(xt,{goal:D}),D.merged_into&&(0,x.jsxs)(A,{children:[`此目标已合并到 #`,D.merged_into,`,原记录与依据保留。`,(0,x.jsxs)(`button`,{className:`text-button`,onClick:()=>r(D.merged_into),children:[`查看保留目标 #`,D.merged_into]})]}),o?(0,x.jsx)(Ot,{goal:o,catalog:t,busy:d,locked:m||!!T.error,cancel:()=>s(null),submit:t=>void j(`/goals/${e}`,{...t,expected_revision:o.revision})}):(0,x.jsxs)(x.Fragment,{children:[(0,x.jsx)(`p`,{className:`goal-content goal-detail-content`,children:D.content}),(0,x.jsx)(Ct,{goal:D,catalog:t}),(0,x.jsxs)(`div`,{className:`goal-time-lines`,children:[(0,x.jsxs)(`p`,{children:[`创建时间:`,(0,x.jsx)(N,{value:D.created_at})]}),(0,x.jsxs)(`p`,{children:[`最后更新:`,(0,x.jsx)(N,{value:D.updated_at})]}),D.state!==`open`&&(0,x.jsxs)(`p`,{children:[`结束时间:`,(0,x.jsx)(N,{value:D.closed_at}),` ·`,` `,D.closed_by?Se[D.closed_by]||D.closed_by:`操作者未知`]})]}),D.overdue&&(0,x.jsx)(`p`,{className:`goal-help`,children:`已过期只是标记,目标仍未结束。可修改截止时间,或明确完成、放弃。`}),!D.merged_into&&(0,x.jsxs)(`div`,{className:`actions`,children:[(0,x.jsx)(`button`,{className:`secondary`,disabled:O,onClick:()=>{s(D),w(``)},children:`编辑目标`}),D.state===`open`&&(0,x.jsxs)(x.Fragment,{children:[(0,x.jsx)(`button`,{disabled:O,onClick:()=>u({kind:`completed`,goal:D}),children:`完成目标`}),(0,x.jsx)(`button`,{className:`danger-text`,disabled:O,onClick:()=>u({kind:`abandoned`,goal:D}),children:`放弃目标`})]})]})]})]}),(0,x.jsx)(Tt,{goal:D,locked:O||!!o||!!c,openMemory:n,clear:e=>u({kind:`basis_clear`,goal:D,annotation:e})}),(0,x.jsx)(Et,{goal:D,openGoal:r}),!D.merged_into&&D.possible_duplicate_ids.length>0&&(0,x.jsxs)(`section`,{className:`panel goals-panel`,children:[(0,x.jsx)(`h2`,{children:`可能重复`}),(0,x.jsx)(`p`,{className:`goal-help`,children:`相似不代表同一件事。核对双方内容、人物和时间后,再决定合并或驳回。`}),D.possible_duplicate_ids.map(n=>(0,x.jsx)(vn,{goal:D,otherId:n,locked:O||!!o,catalog:t,openGoal:r,merge:e=>u({kind:`merge`,goal:D,other:e}),dismiss:t=>void j(`/goals/${e}/duplicates/${t.id}/dismiss`,{expected_revision:D.revision,other_revision:t.revision},`POST`)},`${D.revision}-${n}`))]}),(0,x.jsx)(dn,{goal:D,catalog:t,openMemory:n},`sources-${D.revision}`),(0,x.jsxs)(`section`,{className:`panel goals-panel`,children:[(0,x.jsx)(`h2`,{children:`同证据的承诺记忆`}),(0,x.jsx)(`p`,{className:`goal-help`,children:`与目标使用同一来源消息的角色记忆,保留当时依据修订。目标完成不会自动改写记忆。`}),!D.promise_memories.length&&(0,x.jsx)(`p`,{children:`暂无关联记忆。`}),D.promise_memories.map(e=>(0,x.jsxs)(`article`,{className:`goal-operation`,children:[(0,x.jsx)(`p`,{className:`goal-content`,children:e.content}),(0,x.jsxs)(`p`,{className:`muted`,children:[`依据修订 `,e.revision,` · 当前修订`,` `,e.current_revision,` · `,E(e.lifecycle),e.revision===e.current_revision?``:` · 依据已变化`]}),(0,x.jsxs)(`button`,{className:`text-button`,onClick:()=>n(e.id),children:[`查看记忆 #`,e.id]})]},e.id))]}),(0,x.jsxs)(`section`,{className:`panel goals-panel`,children:[(0,x.jsx)(`h2`,{children:`合并进来的目标`}),!D.merged_goals.length&&(0,x.jsx)(`p`,{className:`goal-help`,children:`暂无合并记录。`}),D.merged_goals.map(e=>(0,x.jsxs)(`article`,{className:`goal-operation`,children:[(0,x.jsx)(`p`,{className:`goal-content`,children:e.content}),(0,x.jsx)(Ct,{goal:e,catalog:t}),(0,x.jsxs)(`button`,{className:`text-button`,onClick:()=>r(e.id),children:[`查看原目标 #`,e.id]})]},e.id))]}),(0,x.jsx)(At,{goalId:D.id,openGoal:r}),(0,x.jsx)(`section`,{className:`panel goals-panel`,children:(0,x.jsxs)(`details`,{children:[(0,x.jsxs)(`summary`,{children:[`提醒计划 · `,D.reminder_plans.length,` 条`]}),(0,x.jsx)(`p`,{className:`goal-help`,children:`计划到点才发布提醒,尚未发布的计划不属于“待取走”提醒。`}),D.reminder_plans.map(e=>(0,x.jsxs)(`div`,{className:`goal-operation`,children:[(0,x.jsxs)(`p`,{children:[gt[e.reminder_kind],` ·`,` `,{scheduled:`待发布`,published:`已发布`,skipped:`已跳过`,cancelled:`已取消`}[e.status]||e.status]}),(0,x.jsx)(N,{value:e.scheduled_at})]},e.id))]})}),(0,x.jsx)(_n,{id:D.id,catalog:t},`operations-${D.revision}`)]}),c&&(0,x.jsxs)(ee,{title:c.kind===`basis_clear`?`清除目标依据标注`:c.kind===`merge`?`确认合并目标`:c.kind===`completed`?`完成目标`:`放弃目标`,closeDisabled:d,onClose:()=>u(null),children:[b&&(0,x.jsx)(A,{error:!0,children:b}),c.kind===`basis_clear`?(0,x.jsxs)(`div`,{className:`goal-confirm`,children:[(0,x.jsx)(`p`,{children:c.annotation.text}),(0,x.jsx)(`p`,{children:`清除只撤下这次观察的标注并保留修订记录,不改变目标正文、状态、截止时间或提醒;依据再次变化时仍可能出现新标注。`}),(0,x.jsxs)(`div`,{className:`actions`,children:[(0,x.jsx)(`button`,{className:`primary`,disabled:d||m,onClick:()=>void j(`/goals/${e}/basis-annotations/${c.annotation.id}`,{expected_revision:c.goal.revision},`DELETE`),children:`确认清除依据标注`}),(0,x.jsx)(`button`,{disabled:d,onClick:()=>u(null),children:`取消`})]})]}):c.kind===`merge`?(0,x.jsxs)(`div`,{className:`goal-confirm`,children:[(0,x.jsxs)(`p`,{children:[`保留较早的目标 #`,[c.goal,c.other].sort((e,t)=>Date.parse(e.created_at)-Date.parse(t.created_at)||e.id-t.id)[0].id,`,另一条保留为指向它的合并记录。`]}),(0,x.jsx)(`p`,{children:`来源与待取提醒归入保留目标,重复提醒可能取消;已取走的记录仍保留。保留目标的正文不自动拼接;明确截止时间补入空值,两方都设提前量时采用较大的分钟数。`}),[c.goal,c.other].map(e=>(0,x.jsxs)(`div`,{children:[(0,x.jsxs)(`h3`,{children:[`目标 #`,e.id,` · 修订 `,e.revision]}),(0,x.jsx)(`p`,{className:`goal-content`,children:e.content}),(0,x.jsx)(Ct,{goal:e,catalog:t})]},e.id)),(0,x.jsxs)(`div`,{className:`actions`,children:[(0,x.jsx)(`button`,{className:`primary`,disabled:d||m,onClick:()=>void j(`/goals/${e}/duplicates/${c.other.id}/merge`,{expected_revision:c.goal.revision,other_revision:c.other.revision},`POST`),children:`确认合并`}),(0,x.jsx)(`button`,{className:`secondary`,disabled:d,onClick:()=>u(null),children:`取消`})]})]}):(0,x.jsxs)(`div`,{className:`goal-confirm`,children:[(0,x.jsx)(`p`,{children:c.goal.content}),(0,x.jsxs)(`p`,{children:[`确认`,c.kind===`completed`?`完成`:`放弃`,`后,尚未取走的提醒会取消,已取走的记录保留。这是明确改变目标状态。`]}),(0,x.jsxs)(`div`,{className:`actions`,children:[(0,x.jsx)(`button`,{className:`primary`,disabled:d||m,onClick:()=>void j(`/goals/${e}`,{state:c.kind,expected_revision:c.goal.revision}),children:c.kind===`completed`?`确认完成`:`确认放弃`}),(0,x.jsx)(`button`,{className:`secondary`,disabled:d,onClick:()=>u(null),children:`取消`})]})]})]})]})}var bn={state:`open`,kind:``,overdue:``,due_soon:``,possible_duplicate:``,entry_id:``,deadline_from:``,deadline_to:``},xn=e=>new URLSearchParams(Object.entries(e).filter(([,e])=>e!==``)).toString();function Sn({initialId:e=null,openMemory:t}){let n=y(`/catalog`),[r,i]=(0,l.useState)(e),[a,o]=(0,l.useState)(bn),[s,c]=(0,l.useState)(xn(bn)),[u,d]=(0,l.useState)(0),[f,p]=(0,l.useState)(``),[m,h]=(0,l.useState)(!1),[b,S]=(0,l.useState)(!1),[C,w]=(0,l.useState)(``),[E,D]=(0,l.useState)(null),O=y(r===null?`/goals?${s}&limit=30&offset=${u}`:null,u===0?2e3:0),k=(e,t)=>o(n=>({...n,[e]:t}));function M(e){if(e.preventDefault(),a.deadline_from&&a.deadline_to&&a.deadline_from>a.deadline_to){p(`开始日期不能晚于结束日期`);return}p(``),d(0),c(xn(a)),O.refresh()}async function N(e){if(!b){S(!0),w(``);try{let t=await g(`/goals`,_(`POST`,e));D(t),h(!1),O.refresh()}catch(e){w(v(e))}finally{S(!1)}}}return(0,x.jsxs)(`div`,{className:`goal-workspace`,children:[n.error&&(0,x.jsxs)(A,{error:!0,children:[`人物与入口名称读取失败:`,n.error,(0,x.jsx)(`button`,{className:`text-button`,onClick:n.refresh,children:`重试人物与入口`})]}),r===null?(0,x.jsxs)(x.Fragment,{children:[(0,x.jsxs)(`section`,{className:`panel goals-panel`,children:[(0,x.jsxs)(`div`,{className:`panel-heading`,children:[(0,x.jsx)(`h2`,{children:`目标与询问`}),(0,x.jsx)(`button`,{className:`primary`,onClick:()=>{w(``),h(!0)},children:`创建目标或询问`})]}),(0,x.jsx)(`p`,{className:`goal-help`,children:`所有入口共享目标。管理员创建的目标不自动合并,相似项仅标为“可能重复”。已过期只是标记,需要明确完成或放弃。`}),(0,x.jsxs)(`form`,{className:`goal-filters`,onSubmit:M,children:[(0,x.jsxs)(`label`,{children:[`目标状态`,(0,x.jsxs)(`select`,{value:a.state,onChange:e=>k(`state`,e.target.value),children:[(0,x.jsx)(`option`,{value:``,children:`全部状态`}),Object.entries(pt).map(([e,t])=>(0,x.jsx)(`option`,{value:e,children:t},e))]})]}),(0,x.jsxs)(`label`,{children:[`目标类型`,(0,x.jsxs)(`select`,{value:a.kind,onChange:e=>k(`kind`,e.target.value),children:[(0,x.jsx)(`option`,{value:``,children:`全部类型`}),Object.entries(mt).map(([e,t])=>(0,x.jsx)(`option`,{value:e,children:t},e))]})]}),[[`overdue`,`是否过期`],[`due_soon`,`是否临近`],[`possible_duplicate`,`是否可能重复`]].map(([e,t])=>(0,x.jsxs)(`label`,{children:[t,(0,x.jsxs)(`select`,{value:a[e],onChange:t=>k(e,t.target.value),children:[(0,x.jsx)(`option`,{value:``,children:`全部`}),(0,x.jsx)(`option`,{value:`true`,children:`是`}),(0,x.jsx)(`option`,{value:`false`,children:`否`})]})]},e)),(0,x.jsxs)(`label`,{children:[`产生的入口`,(0,x.jsxs)(`select`,{value:a.entry_id,onChange:e=>k(`entry_id`,e.target.value),children:[(0,x.jsx)(`option`,{value:``,children:`全部入口`}),n.data?.entries.map(e=>(0,x.jsx)(`option`,{value:e.id,children:e.name||e.id},e.id))]})]}),(0,x.jsxs)(`label`,{children:[`截止日期从`,(0,x.jsx)(`input`,{type:`date`,value:a.deadline_from,onChange:e=>k(`deadline_from`,e.target.value)})]}),(0,x.jsxs)(`label`,{children:[`截止日期至`,(0,x.jsx)(`input`,{type:`date`,value:a.deadline_to,onChange:e=>k(`deadline_to`,e.target.value)})]}),(0,x.jsx)(`p`,{className:`goal-help full-width`,children:`日期范围按角色时区解释,结束日期包含当天。临近范围包含已过期目标;选择“是否过期:否”可排除。`}),(0,x.jsxs)(`div`,{className:`actions full-width`,children:[(0,x.jsx)(`button`,{className:`primary`,children:`筛选目标`}),(0,x.jsx)(`button`,{type:`button`,className:`secondary`,onClick:O.refresh,children:`刷新目标`}),(0,x.jsx)(`button`,{type:`button`,className:`text-button`,onClick:()=>{o(bn),c(xn(bn)),d(0),p(``),O.refresh()},children:`重置筛选`})]})]})]}),E&&(0,x.jsx)(wt,{receipt:E,openGoal:i}),(f||O.error)&&(0,x.jsxs)(A,{error:!0,children:[f||O.error,O.error&&O.data&&`。以下为上次读取结果,可能尚未更新。`]}),(0,x.jsx)(`div`,{className:`list-heading`,children:(0,x.jsx)(`span`,{children:O.data?`共 ${O.data.total} 个目标与询问`:O.error?`目标暂不可读取`:`正在读取目标…`})}),O.data?.items.length===0&&(0,x.jsx)(j,{title:`当前范围没有目标或询问`,children:`可以创建目标,或在试用对话中让角色答应一件事,学习后在这里查看。`}),(0,x.jsx)(`div`,{className:`goal-grid`,children:O.data?.items.map(e=>(0,x.jsxs)(`article`,{className:`panel goals-panel`,children:[(0,x.jsx)(xt,{goal:e}),(0,x.jsx)(`h3`,{className:`goal-content`,children:e.content}),(0,x.jsx)(Ct,{goal:e,catalog:n.data}),(0,x.jsxs)(`button`,{className:`text-button`,onClick:()=>i(e.id),children:[`查看目标 #`,e.id]})]},e.id))}),O.data&&(0,x.jsx)(T,{total:O.data.total,offset:u,change:d})]}):(0,x.jsx)(yn,{id:r,catalog:n.data,openMemory:t,openGoal:i,onBack:()=>i(null),onChange:O.refresh},r),m&&(0,x.jsxs)(ee,{title:`创建目标或询问`,onClose:()=>h(!1),closeDisabled:b,children:[(0,x.jsx)(`p`,{className:`goal-help`,children:`手动创建只会标记可能重复,保留每一条目标供你核对。`}),C&&(0,x.jsx)(A,{error:!0,children:C}),(0,x.jsx)(Ot,{catalog:n.data,busy:b,submit:e=>void N(e),cancel:()=>h(!1)})]})]})}var Cn={host:`宿主提供`,first_report:`自首次报告起`},wn={start:`开始活动`,replace:`更换活动`,update:`更新状态`,heartbeat:`心跳`,end:`结束活动`},Tn={activity:`活动`,mood:`情绪`,started_at:`开始时间`,start_time_basis:`开始时间依据`};function En(e){let t=Math.max(0,Math.floor(e));return[[Math.floor(t/86400),`天`],[Math.floor(t/3600)%24,`小时`],[Math.floor(t/60)%60,`分钟`],[t%60,`秒`]].filter(([e])=>e!==0).map(([e,t])=>`${e} ${t}`).join(` `)||`0 秒`}function Dn(e,t=!1){return e===null?`无`:typeof e==`boolean`?e?`是(true)`:`否(false)`:e===``?`空文本`:t&&typeof e==`string`?`“${e}”`:String(e)}function On({state:e}){return(0,x.jsx)(x.Fragment,{children:e.mood===null?e.mood_updated_at?`已清空`:`未报告`:Dn(e.mood)})}function kn({error:e,retained:t}){return e?(0,x.jsxs)(A,{error:!0,children:[`当前状态读取失败:`,e,`。`,t&&`以下为上次读取结果,可能尚未更新。`]}):null}function An({state:e}){return(0,x.jsxs)(x.Fragment,{children:[e.possibly_stale&&(0,x.jsxs)(`div`,{className:`state-stale`,children:[(0,x.jsx)(M,{tone:`warning`,children:`可能过时`}),(0,x.jsxs)(`p`,{children:[`超过 `,e.stale_after_minutes,` `,`分钟未收到报告,保留宿主最后报告的活动。`]})]}),(0,x.jsxs)(`dl`,{className:`state-fields`,children:[(0,x.jsxs)(`div`,{className:`full-width`,children:[(0,x.jsx)(`dt`,{children:`当前活动`}),(0,x.jsx)(`dd`,{className:`state-activity`,children:e.activity}),(0,x.jsxs)(`dd`,{className:`state-field-time`,children:[`活动更新时间:`,(0,x.jsx)(N,{value:e.activity_updated_at})]})]}),(0,x.jsxs)(`div`,{children:[(0,x.jsx)(`dt`,{children:`情绪`}),(0,x.jsx)(`dd`,{children:(0,x.jsx)(On,{state:e})}),(0,x.jsxs)(`dd`,{className:`state-field-time`,children:[`情绪更新时间:`,(0,x.jsx)(N,{value:e.mood_updated_at})]})]}),(0,x.jsxs)(`div`,{children:[(0,x.jsx)(`dt`,{children:`活动持续时长`}),(0,x.jsx)(`dd`,{children:En(e.duration_seconds)})]}),(0,x.jsxs)(`div`,{children:[(0,x.jsx)(`dt`,{children:`开始时间`}),(0,x.jsx)(`dd`,{children:(0,x.jsx)(N,{value:e.started_at})})]}),(0,x.jsxs)(`div`,{children:[(0,x.jsx)(`dt`,{children:`开始时间依据`}),(0,x.jsx)(`dd`,{children:Cn[e.start_time_basis]})]}),(0,x.jsxs)(`div`,{className:`full-width`,children:[(0,x.jsx)(`dt`,{children:`最后更新时间`}),(0,x.jsx)(`dd`,{children:(0,x.jsx)(N,{value:e.updated_at})})]}),(0,x.jsxs)(`div`,{children:[(0,x.jsx)(`dt`,{children:`最后报告宿主`}),(0,x.jsx)(`dd`,{children:e.host??`未知`})]}),(0,x.jsxs)(`div`,{children:[(0,x.jsx)(`dt`,{children:`最后报告入口`}),(0,x.jsx)(`dd`,{children:e.entry_id??`未知`})]})]}),(0,x.jsx)(`h3`,{children:`细节`}),Object.keys(e.details).length?(0,x.jsx)(`dl`,{className:`state-fields state-detail-fields`,children:Object.entries(e.details).map(([e,t])=>(0,x.jsxs)(`div`,{children:[(0,x.jsx)(`dt`,{children:e}),(0,x.jsx)(`dd`,{children:Dn(t.value)}),(0,x.jsxs)(`dd`,{className:`state-field-time`,children:[`更新时间:`,(0,x.jsx)(N,{value:t.updated_at})]})]},e))}):(0,x.jsx)(`p`,{className:`quiet`,children:`暂无细节报告`})]})}function jn(){let e=y(`/state`,2e3),t=e.data;return(0,x.jsxs)(`div`,{className:`state-summary`,children:[(0,x.jsx)(kn,{error:e.error,retained:t!==null}),e.error&&(0,x.jsx)(`button`,{className:`text-button`,onClick:e.refresh,children:`重试当前状态`}),!t&&!e.error&&(0,x.jsx)(`p`,{role:`status`,children:`正在读取当前状态…`}),t&&(t.activity?(0,x.jsxs)(x.Fragment,{children:[(0,x.jsx)(`p`,{className:`state-summary-activity`,children:t.activity}),t.possibly_stale&&(0,x.jsx)(M,{tone:`warning`,children:`可能过时`}),(0,x.jsxs)(`dl`,{className:`state-summary-fields`,children:[(0,x.jsxs)(`div`,{children:[(0,x.jsx)(`dt`,{children:`情绪`}),(0,x.jsx)(`dd`,{children:(0,x.jsx)(On,{state:t})})]}),(0,x.jsxs)(`div`,{children:[(0,x.jsx)(`dt`,{children:`活动持续时长`}),(0,x.jsx)(`dd`,{children:En(t.duration_seconds)})]})]}),t.start_time_basis===`first_report`&&(0,x.jsx)(`p`,{className:`muted`,children:`持续时长自首次报告起计算。`})]}):(0,x.jsx)(`p`,{className:`quiet`,children:`暂无宿主报告的当前活动`})),(0,x.jsx)(`a`,{className:`text-button`,href:`#/state`,children:`查看状态与报告历史`})]})}function Mn({report:e}){let t=[...Object.entries(Tn).flatMap(([t,n])=>{let r=e.changes[t];return r?[{key:t,label:n,...r}]:[]}),...Object.entries(e.changes.details||{}).map(([e,t])=>({key:`details.${e}`,label:`细节 · ${e}`,...t}))],n=(e,t)=>t===null?`无`:e===`started_at`?(0,x.jsx)(N,{value:String(t)}):e===`start_time_basis`?Cn[t]||String(t):Dn(t,!0);return(0,x.jsxs)(`article`,{className:`state-report`,"aria-label":`报告 #${e.id}`,children:[(0,x.jsxs)(`div`,{className:`panel-heading`,children:[(0,x.jsxs)(`h3`,{children:[wn[e.action],` · `,e.action]}),(0,x.jsxs)(`span`,{className:`muted`,children:[`报告 #`,e.id,` · `,e.method]})]}),(0,x.jsx)(`p`,{className:`state-report-time`,children:(0,x.jsx)(N,{value:e.reported_at})}),(0,x.jsxs)(`dl`,{className:`state-report-source`,children:[(0,x.jsxs)(`div`,{children:[(0,x.jsx)(`dt`,{children:`宿主`}),(0,x.jsx)(`dd`,{children:e.host??`未知`})]}),(0,x.jsxs)(`div`,{children:[(0,x.jsx)(`dt`,{children:`入口`}),(0,x.jsx)(`dd`,{children:e.entry_id??`未知`})]})]}),t.length?(0,x.jsx)(`dl`,{className:`state-changes`,children:t.map(e=>(0,x.jsxs)(`div`,{children:[(0,x.jsx)(`dt`,{children:e.label}),(0,x.jsxs)(`dd`,{children:[(0,x.jsx)(`span`,{className:`muted`,children:`原值`}),(0,x.jsx)(`span`,{children:n(e.key,e.before)})]}),(0,x.jsxs)(`dd`,{children:[(0,x.jsx)(`span`,{className:`muted`,children:`现值`}),(0,x.jsx)(`span`,{children:n(e.key,e.after)})]})]},e.key))}):(0,x.jsx)(`p`,{className:`muted`,children:`无字段值变化,本次报告仍已记录。`})]})}function Nn(){let e=y(`/state`,2e3),[t,n]=(0,l.useState)(0),r=y(`/state/reports?limit=30&offset=${t}`,t===0?2e3:0);return(0,x.jsx)(x.Fragment,{children:(0,x.jsxs)(`div`,{className:`state-page`,children:[(0,x.jsxs)(`section`,{className:`panel state-panel`,"aria-labelledby":`current-state-title`,children:[(0,x.jsxs)(`div`,{className:`panel-heading`,children:[(0,x.jsx)(`h2`,{id:`current-state-title`,children:`当前状态`}),(0,x.jsx)(`button`,{className:`text-button`,disabled:e.loading,onClick:e.refresh,children:`刷新当前状态`})]}),(0,x.jsx)(`p`,{className:`state-help`,children:`活动持续时长与情绪、细节的更新时间分别记录。时间按角色时区显示,保留 UTC 偏移。`}),(0,x.jsx)(kn,{error:e.error,retained:e.data!==null}),!e.data&&!e.error&&(0,x.jsx)(`p`,{role:`status`,children:`正在读取当前状态…`}),e.data&&(e.data.activity?(0,x.jsx)(An,{state:e.data}):(0,x.jsx)(j,{title:`暂无宿主报告的当前活动`,children:`宿主开始活动后会显示在这里;结束活动后,报告历史仍会保留。`}))]}),(0,x.jsxs)(`section`,{className:`panel state-panel`,"aria-labelledby":`state-reports-title`,children:[(0,x.jsxs)(`div`,{className:`panel-heading`,children:[(0,x.jsxs)(`h2`,{id:`state-reports-title`,children:[`报告历史`,r.data?` · ${r.data.total} 条`:``]}),(0,x.jsx)(`button`,{className:`text-button`,disabled:r.loading,onClick:()=>t===0?r.refresh():n(0),children:`刷新报告历史`})]}),(0,x.jsxs)(`p`,{className:`state-help`,children:[`按报告顺序从新到旧排列,列出字段值的实际变化。心跳也会保留记录。`,t===0?`最新一页自动刷新。`:`较早报告暂停自动刷新;刷新历史可回到最新一页。`]}),r.error&&(0,x.jsxs)(A,{error:!0,children:[`报告历史读取失败:`,r.error,`。`,r.data&&`以下为上次读取的报告。`]}),!r.data&&!r.error&&(0,x.jsx)(`p`,{role:`status`,children:`正在读取报告历史…`}),r.data?.items.length===0&&(0,x.jsx)(j,{title:`暂无报告历史`}),r.data?.items.map(e=>(0,x.jsx)(Mn,{report:e},e.id)),r.data&&(0,x.jsx)(T,{total:r.data.total,offset:t,limit:30,change:n})]})]})})}function Pn({initialQuery:e=``,openMemory:t=()=>{}}){let n=new URLSearchParams(e),r=Number(n.get(`id`)),[i,a]=(0,l.useState)(Number.isSafeInteger(r)&&r>0?r:null),o=n.get(`tab`),[s,c]=(0,l.useState)(o===`goals`||o===`notifications`?o:`state`);function u(e){a(e),c(`goals`)}return(0,x.jsxs)(x.Fragment,{children:[(0,x.jsx)(`div`,{className:`page-heading`,children:(0,x.jsxs)(`div`,{children:[(0,x.jsx)(`p`,{className:`eyebrow`,children:`此刻与接下来的事`}),(0,x.jsx)(`h1`,{children:`状态与目标`}),(0,x.jsx)(`p`,{children:`当前状态完全由宿主报告,管理员不能编辑。状态与目标由所有入口共享。`})]})}),(0,x.jsxs)(`nav`,{className:`learning-tabs`,"aria-label":`状态与目标分区`,children:[(0,x.jsx)(`button`,{"aria-pressed":s===`state`,onClick:()=>c(`state`),children:`当前状态`}),(0,x.jsx)(`button`,{"aria-pressed":s===`goals`,onClick:()=>{a(null),c(`goals`)},children:`目标与询问`}),(0,x.jsx)(`button`,{"aria-pressed":s===`notifications`,onClick:()=>c(`notifications`),children:`提醒`})]}),s===`state`?(0,x.jsx)(Nn,{}):s===`goals`?(0,x.jsx)(Sn,{initialId:i,openMemory:t},i):(0,x.jsx)(At,{openGoal:u})]})}function Fn({goals:e,error:t=``}){let n=y(`/notifications?status=pending&limit=1&offset=0`,2e3),r=e?.filter(e=>e.state===`open`&&!e.merged_into).slice(0,10);return(0,x.jsxs)(`section`,{className:`panel trial-goals`,"aria-labelledby":`trial-goals-title`,children:[(0,x.jsxs)(`div`,{className:`panel-heading`,children:[(0,x.jsx)(`h2`,{id:`trial-goals-title`,children:`未结束的目标与询问`}),(0,x.jsx)(M,{children:r?.length??`—`})]}),(0,x.jsx)(`p`,{className:`goal-help`,children:`角色答应一件事 → 学习形成目标 → 到点发布提醒。可在学习结束后点击目标查看来源;全部入口共享,优先展示临近、过期的目标,最多 10 个。`}),t&&(0,x.jsxs)(A,{error:!0,children:[`目标暂不可读取:`,t,`。`,r&&`以下为上次读取结果。`]}),!r&&!t&&(0,x.jsx)(`p`,{role:`status`,children:`正在读取目标…`}),r?.length===0&&(0,x.jsx)(`p`,{className:`quiet`,children:`暂无未结束的目标或询问`}),r?.map(e=>(0,x.jsxs)(`a`,{className:`trial-goal-link`,href:`#/state?tab=goals&id=${e.id}`,children:[(0,x.jsx)(xt,{goal:e}),(0,x.jsx)(`p`,{className:`goal-content`,children:e.content}),(0,x.jsx)(`small`,{children:(0,x.jsx)(St,{goal:e})}),(0,x.jsx)(`span`,{className:`text-button`,children:`查看目标与来源 ↗`})]},e.id)),n.error&&(0,x.jsxs)(`p`,{className:`danger-text`,children:[`提醒暂不可读取:`,n.error,(0,x.jsx)(`button`,{className:`text-button`,onClick:n.refresh,children:`重试提醒`})]}),(0,x.jsxs)(`div`,{className:`actions`,children:[(0,x.jsx)(`a`,{className:`text-button`,href:`#/state?tab=goals`,children:`查看全部目标与询问`}),(0,x.jsxs)(`a`,{className:`text-button`,href:`#/state?tab=notifications`,children:[`查看提醒`,n.data?.total===void 0?``:` · ${n.data.total} 条待取走`]})]}),(0,x.jsx)(`p`,{className:`goal-help`,children:`提醒被宿主取走不代表已送达或目标完成。这里的自动刷新不会取走提醒。`})]})}function In(e){let t=y(`/trial`),[n,r]=(0,l.useState)(``),[i,a]=(0,l.useState)(!1),[o,s]=(0,l.useState)(``),[c,u]=(0,l.useState)(`group`),[d,f]=(0,l.useState)(``),[p,m]=(0,l.useState)(!1),h=t.data?.entries||[],b=(0,l.useMemo)(()=>{if(!t.data)return null;let e=new Map;for(let n of t.data.speakers){let t=e.get(n.id);e.set(n.id,{...n,is_default:!!(n.is_default||t?.is_default)})}return{...t.data,speakers:[...e.values()]}},[t.data]);(0,l.useEffect)(()=>{!n&&h.length&&r(h[0].id)},[n,h]);async function S(n){n.preventDefault(),m(!0),f(``);try{let n=await g(`/trial/entries`,_(`POST`,{name:o,kind:c}));r(n.id),a(!1),s(``),t.refresh(),e.refreshStatus()}catch(e){f(v(e))}finally{m(!1)}}return(0,x.jsxs)(x.Fragment,{children:[(0,x.jsxs)(`div`,{className:`page-heading`,children:[(0,x.jsxs)(`div`,{children:[(0,x.jsx)(`p`,{className:`eyebrow`,children:`在相处中,慢慢记住`}),(0,x.jsx)(`h1`,{children:`试用对话`}),(0,x.jsx)(`p`,{children:`聊一件小事,看看它如何成为记忆。`})]}),(0,x.jsx)(M,{tone:`purple`,children:h.find(e=>e.id===n)?`${at(h.find(e=>e.id===n).pace)}学习`:`试用入口`})]}),(t.error||d)&&(0,x.jsx)(A,{error:!0,children:t.error||d}),(0,x.jsxs)(`div`,{className:`entry-bar`,children:[(0,x.jsxs)(`label`,{children:[`当前入口`,(0,x.jsxs)(`select`,{"aria-label":`当前入口`,value:n,onChange:e=>r(e.target.value),children:[(0,x.jsx)(`option`,{value:``,disabled:!0,children:`选择试用入口`}),h.map(e=>(0,x.jsx)(`option`,{value:e.id,children:e.name},e.id))]})]}),(0,x.jsx)(`button`,{className:`secondary`,onClick:()=>a(!i),children:`+ 新建入口`}),(0,x.jsx)(`span`,{className:`muted entry-tip`,children:`记忆共同积累,近期消息各自保留`})]}),(i||t.data&&!h.length)&&(0,x.jsxs)(`form`,{className:`inline-form panel`,onSubmit:S,children:[(0,x.jsxs)(`label`,{children:[`入口名称`,(0,x.jsx)(`input`,{value:o,onChange:e=>s(e.target.value),placeholder:`如:试用群聊 A`,required:!0,maxLength:100})]}),(0,x.jsxs)(`label`,{children:[`入口类型`,(0,x.jsxs)(`select`,{value:c,onChange:e=>u(e.target.value),children:[(0,x.jsx)(`option`,{value:`group`,children:`群聊`}),(0,x.jsx)(`option`,{value:`private`,children:`私聊`})]})]}),(0,x.jsx)(`button`,{className:`primary`,disabled:p||!o.trim(),children:`创建入口`})]}),!t.data&&!t.error&&(0,x.jsx)(`p`,{className:`muted`,children:`正在读取试用入口…`}),n&&b&&(0,x.jsx)(Ln,{entryId:n,catalog:b,refreshCatalog:t.refresh,...e},n)]})}function Ln({entryId:e,catalog:t,refreshCatalog:n,status:r,openMemory:i,refreshStatus:a}){let o=y(`/trial/entries/${e}`,2e3),[s,c]=(0,l.useState)(t.speakers.find(e=>e.is_default)?.id||``);(0,l.useEffect)(()=>{s||c(t.speakers.find(e=>e.is_default)?.id||``)},[s,t.speakers]);let[u,d]=(0,l.useState)(!1),[p,m]=(0,l.useState)(``),[h,b]=(0,l.useState)(``),C=`iris.trial.replies.${e}`,[w,T]=(0,l.useState)(()=>{try{return window.localStorage.getItem(C)===`true`}catch{return!1}});function E(e){T(e);try{window.localStorage.setItem(C,String(e))}catch{}}let[D,ee]=(0,l.useState)(!1),[N,P]=(0,l.useState)(``),[F,I]=(0,l.useState)(``),[te,L]=(0,l.useState)(``),[ne,R]=(0,l.useState)(null),[re,ie]=(0,l.useState)(null),[ae,oe]=(0,l.useState)(null),[se,ce]=(0,l.useState)(``),[le,ue]=(0,l.useState)([]),[de,z]=(0,l.useState)(null),[B,V]=(0,l.useState)(!1),[fe,pe]=(0,l.useState)(null),me=Ze(),he=(0,l.useRef)(null);(0,l.useEffect)(()=>()=>he.current?.abort(),[]);function H(e){if(!D){I(``);try{me.add(e)}catch(e){I(v(e))}}}let U=o.data,ge=r?.entries.find(t=>t.entry_id===e),_e=[...new Map([...le,...U?.messages||[]].map(e=>[e.id,e])).values()].sort((e,t)=>e.id-t.id),ve=(0,l.useRef)(null),ye=(0,l.useRef)(!0),be=_e.at(-1)?.id;(0,l.useEffect)(()=>{ye.current&&ve.current&&(ve.current.scrollTop=ve.current.scrollHeight)},[be]);async function xe(e){e.preventDefault(),I(``);try{let e=await g(`/trial/speakers`,_(`POST`,{name:p}));n(),c(e.id),d(!1),m(``)}catch(e){I(v(e))}}async function Se(t,n){P(n?`角色正在回复…`:`正在准备回复材料…`),ce(``),oe(null),ie(null);try{if(n){R(t);let n=await g(`/trial/entries/${e}/reply`,_(`POST`,{message_id:t}));ie(n.prepared),oe(n.diagnostic||null),n.reused&&ce(n.diagnostic?`已复用回复 #${n.diagnostic.reply_message_id};以下诊断来自发布该回复时的准备。`:`已复用已发布回复;原诊断未记录或已按保留期限清理,无法还原当时的判断。`),R(null)}else ie(await g(`/trial/entries/${e}/prepare`,_(`POST`)))}catch(e){let t=e instanceof f&&e.code===`reply_failed`?e.result:void 0;throw t?.diagnostic?(oe(t.diagnostic),ce(`角色回复失败;以下为本次已经完成的回复准备诊断。`)):ce(`本次回复准备结果未返回,无法展示召回判断状态。`),e}}async function Ce(t){if(t.preventDefault(),!h.trim()&&!me.images.length||D||!s)return;let n=new AbortController;he.current=n,ee(!0),I(``),L(``),ie(null),oe(null),ce(``),R(null),P(`正在发送…`);let r=me.images.map(e=>e.key).join(`,`),i=fe?.content===h&&fe.speaker===s&&fe.imageKeys===r?fe:{content:h,speaker:s,imageKeys:r,key:crypto.randomUUID()};pe(i);try{let t=await me.upload(n.signal,P);n.signal.throwIfAborted(),P(`正在发送…`);let r=await g(`/trial/entries/${e}/messages`,{..._(`POST`,{speaker_id:s,content:h,dedupe_key:i.key,media_ids:t}),signal:n.signal});n.signal.throwIfAborted(),b(``),me.clear(),pe(null),L(`消息已接收,学习结果请看右侧批次。`),o.refresh(),a(),await Se(r.message_id,w)}catch(e){n.signal.aborted||I(v(e))}finally{n.signal.aborted||(ee(!1),P(``),o.refresh(),a())}}async function we(){if(ne){ee(!0),I(``);try{await Se(ne,!0)}catch(e){I(v(e))}finally{ee(!1),P(``),o.refresh(),a()}}}async function Te(){I(``);try{let t=await g(`/trial/entries/${e}/learn`,_(`POST`));L(t.paused?`已请求立即学习;模型暂停中,恢复后处理。`:`已请求立即学习,完成情况请看批次状态。`),a()}catch(e){I(v(e))}}async function Ee(){V(!0);try{let t=await g(`/trial/entries/${e}?before=${_e[0].id}`);ue([...t.messages,...le]),z(t.has_older)}catch(e){I(v(e))}finally{V(!1)}}return(0,x.jsxs)(`div`,{className:`trial-grid`,children:[(0,x.jsxs)(`section`,{className:`panel conversation`,children:[(0,x.jsxs)(`div`,{className:`panel-heading`,children:[(0,x.jsxs)(`div`,{children:[(0,x.jsx)(`h2`,{children:U?.entry.name||`正在读取对话…`}),(0,x.jsxs)(`span`,{className:`muted`,children:[U?.entry.kind===`private`?`私聊`:`群聊`,` · 图文试用`]})]}),(0,x.jsxs)(`span`,{className:`live-label`,children:[(0,x.jsx)(`i`,{}),` `,o.error?`连接中断`:U?`入口已就绪`:`正在连接`]})]}),(0,x.jsxs)(`div`,{className:`messages`,"aria-label":`对话记录`,ref:ve,onScroll:e=>{let t=e.currentTarget;ye.current=t.scrollHeight-t.scrollTop-t.clientHeight<60},children:[(de??U?.has_older)&&(0,x.jsx)(`button`,{className:`text-button older`,onClick:Ee,disabled:B,children:`查看更早消息`}),!_e.length&&(0,x.jsx)(j,{title:`从一句话开始`,children:`例如「我下周三去上海出差」。实时节奏会在短暂停顿后开始学习。`}),_e.map(e=>(0,x.jsxs)(`article`,{className:`message ${e.kind===`self_output`?`role`:`user`}`,children:[(0,x.jsxs)(`div`,{className:`message-meta`,children:[(0,x.jsx)(`strong`,{children:e.kind===`self_output`?t.role_name:e.sender_name}),(0,x.jsx)(`time`,{children:S(e.received_at)})]}),(0,x.jsxs)(`div`,{className:`bubble`,children:[e.content,(0,x.jsx)(qe,{media:e.media})]}),(0,x.jsxs)(`div`,{className:`message-state ${[`abandoned`,`refused`].includes(e.learning_state)?`danger-text`:``}`,children:[O(e.learning_state),e.batch_id&&` · 批次 #${e.batch_id}`,e.kind===`self_output`?` · 角色实际输出`:``]})]},e.id))]}),(0,x.jsxs)(`div`,{className:`composer`,children:[(F||o.error)&&(0,x.jsx)(A,{error:!0,children:F||o.error}),te&&(0,x.jsx)(`p`,{className:`receipt`,role:`status`,children:te}),ne&&!D&&(0,x.jsx)(`button`,{className:`secondary`,onClick:we,children:`重试角色回复`}),u&&(0,x.jsxs)(`form`,{className:`inline-form compact`,onSubmit:xe,children:[(0,x.jsxs)(`label`,{children:[`虚拟发言人姓名`,(0,x.jsx)(`input`,{value:p,maxLength:100,required:!0,onChange:e=>m(e.target.value)})]}),(0,x.jsx)(`button`,{className:`secondary`,disabled:!p.trim(),children:`添加发言人`})]}),(0,x.jsxs)(`form`,{onSubmit:Ce,children:[(0,x.jsxs)(`div`,{className:`composer-controls`,children:[(0,x.jsxs)(`label`,{className:`speaker-label`,children:[`发言人`,(0,x.jsx)(`select`,{"aria-label":`发言人`,disabled:D,value:s,onChange:e=>c(e.target.value),children:t.speakers.map(e=>(0,x.jsxs)(`option`,{value:e.id,children:[e.name,t.speakers.filter(t=>t.name===e.name).length>1?` · ${e.id.slice(0,6)}`:``]},e.id))})]}),(0,x.jsx)(`button`,{type:`button`,className:`text-button`,onClick:()=>d(!u),children:`+ 添加`}),(0,x.jsxs)(`label`,{className:`switch`,children:[(0,x.jsx)(`input`,{type:`checkbox`,checked:w,disabled:D,onChange:e=>E(e.target.checked)}),(0,x.jsx)(`span`,{children:`角色回复`})]})]}),(0,x.jsx)(`textarea`,{"aria-label":`消息内容`,placeholder:`说说正在发生的事…`,value:h,onChange:e=>b(e.target.value),onPaste:e=>{let t=Array.from(e.clipboardData.files);t.length&&(e.preventDefault(),H(t))},rows:3,disabled:D}),(0,x.jsx)(Qe,{images:me.images,busy:D,add:H,remove:me.remove}),(0,x.jsxs)(`div`,{className:`composer-bottom`,children:[(0,x.jsx)(`span`,{className:`muted`,children:N||`接收后自动排队学习`}),(0,x.jsxs)(`button`,{className:`primary`,disabled:D||!h.trim()&&!me.images.length||!s,"aria-label":`发送消息`,children:[D?`处理中…`:`发送`,` `,(0,x.jsx)(`span`,{"aria-hidden":!0,children:`↑`})]})]})]})]})]}),(0,x.jsxs)(`aside`,{className:`trial-aside`,"aria-label":`学习与回复上下文`,children:[(0,x.jsxs)(`section`,{className:`panel`,children:[(0,x.jsxs)(`div`,{className:`panel-heading`,children:[(0,x.jsx)(`h2`,{children:`学习动态`}),(0,x.jsx)(`button`,{className:`text-button`,onClick:Te,children:`立即学习`})]}),(0,x.jsxs)(`div`,{className:`queue-summary`,children:[(0,x.jsx)(`strong`,{children:ge?.pending_count??`—`}),(0,x.jsx)(`span`,{children:`条待学习`}),(0,x.jsx)(M,{tone:ge?.current_batch?`purple`:``,children:ge?.current_batch?`批次 #${ge.current_batch.id}`:`等待新消息`})]}),ge?.current_batch&&(0,x.jsxs)(`p`,{className:`batch-state`,children:[k(ge.current_batch),ge.current_batch.next_retry_at&&(0,x.jsxs)(`small`,{children:[`下次尝试:`,S(ge.current_batch.next_retry_at)]})]}),ge?.latest_batch&&(0,x.jsxs)(`div`,{className:`batch-result`,children:[(0,x.jsxs)(`span`,{className:`muted`,children:[`最近结束 · #`,ge.latest_batch.id]}),(0,x.jsx)(`p`,{children:k(ge.latest_batch)}),ge.latest_batch.last_error&&(0,x.jsx)(`small`,{children:ge.latest_batch.last_error})]}),(0,x.jsx)(`p`,{className:`fine-print`,children:`学习成功也可能没有值得记住的内容。`}),(0,x.jsx)(st,{value:ge?.queue_wait})]}),(0,x.jsx)(Fn,{goals:U?.goals,error:o.error}),(0,x.jsxs)(`section`,{className:`panel`,children:[(0,x.jsxs)(`div`,{className:`panel-heading`,children:[(0,x.jsx)(`h2`,{children:`新形成与更新`}),(0,x.jsx)(M,{children:U?.recent_memories.length||0})]}),(0,x.jsx)(`p`,{className:`muted section-help`,children:`本入口最近 12 条,点击查看来源`}),U?.recent_memories.length?U.recent_memories.map(e=>(0,x.jsxs)(`button`,{className:`memory-preview`,onClick:()=>i(e.id),children:[(0,x.jsxs)(`span`,{className:`tiny-label`,children:[e.kind,` · #`,e.id]}),(0,x.jsx)(`span`,{children:e.content}),(0,x.jsxs)(`small`,{children:[S(e.updated_at),` `,(0,x.jsx)(`span`,{"aria-hidden":!0,children:`↗`})]})]},e.id)):(0,x.jsx)(`p`,{className:`quiet`,children:`暂时还没有形成记忆。`})]}),(0,x.jsxs)(`section`,{className:`panel`,children:[(0,x.jsxs)(`div`,{className:`panel-heading`,children:[(0,x.jsx)(`h2`,{children:`这次回复准备`}),(0,x.jsx)(`span`,{className:`muted`,children:re?`${re.memories.length} 条召回`:ae?`当次诊断`:se?`未返回材料`:D?`准备中`:`等待发送`})]}),!re&&!ae?(0,x.jsx)(`p`,{className:`quiet`,children:se||(D?`正在准备回复材料…`:`发送消息后展示召回结果。`)}):(0,x.jsxs)(x.Fragment,{children:[se&&(0,x.jsx)(A,{children:se}),(0,x.jsx)(dt,{value:ae?.judgment||re?.judgment}),ae&&(0,x.jsx)(We,{value:ae,openMemory:i}),re?.memories.length===0&&(0,x.jsx)(`p`,{className:`quiet`,children:`没有额外召回记忆,近期原文仍在对话上下文中。`}),re?.memories.map(e=>(0,x.jsxs)(`div`,{children:[(0,x.jsxs)(`button`,{className:`memory-preview`,onClick:()=>i(e.id),children:[(0,x.jsx)(M,{tone:`purple`,children:e.reason===`person_highlight`?`人物要点`:`与当前内容相关`}),(0,x.jsx)(`span`,{children:e.content})]}),(0,x.jsx)(ft,{value:e.subject_annotations})]},e.id)),(0,x.jsx)(`p`,{className:`fine-print`,children:`被召回不等于被使用,也不代表内容已证实。`}),re?.hints.filter(e=>e.code!==`recall_judgment`).map((e,t)=>(0,x.jsx)(`p`,{className:`fine-print`,children:e.message},t))]})]}),(0,x.jsxs)(`section`,{className:`panel context-panel`,children:[(0,x.jsx)(`h2`,{children:`角色上下文`}),(0,x.jsx)($e,{}),(0,x.jsxs)(`details`,{open:!0,children:[(0,x.jsx)(`summary`,{children:`当前状态`}),(0,x.jsx)(jn,{})]})]})]})]})}function Rn({value:e}){return e?(0,x.jsxs)(x.Fragment,{children:[(0,x.jsxs)(`table`,{className:`image-usage`,children:[(0,x.jsx)(`caption`,{children:`图片理解用量`}),(0,x.jsx)(`thead`,{children:(0,x.jsxs)(`tr`,{children:[(0,x.jsx)(`th`,{scope:`col`,children:`指标`}),(0,x.jsx)(`th`,{scope:`col`,children:`今日`}),(0,x.jsx)(`th`,{scope:`col`,children:`本周`})]})}),(0,x.jsx)(`tbody`,{children:[[`调用次数`,e=>e.calls],[`token(输入+输出)`,e=>e.tokens.toLocaleString()],[`累计耗时`,e=>C(e.duration_ms)],[`失败调用`,e=>e.failures],[`其中被拒绝`,e=>e.refusals],[`超时调用`,e=>e.timeouts],[`未返回 token 用量的调用`,e=>e.calls_without_usage]].map(([t,n])=>(0,x.jsxs)(`tr`,{children:[(0,x.jsx)(`th`,{scope:`row`,children:t}),(0,x.jsx)(`td`,{children:n(e.today)}),(0,x.jsx)(`td`,{children:n(e.week)})]},t))})]}),(0,x.jsx)(`p`,{className:`fine-print`,children:`按角色时区统计,本周自周一开始。含重试、连接测试和恢复探测;未返回的 token 用量不作推算。`})]}):(0,x.jsx)(`p`,{children:`暂时无法读取图片用量。`})}var zn={manual:`手动维护`,scheduled:`定时维护`,catchup:`启动补跑`},Bn={decay:`衰减与状态转换`,expiry:`到期删除`,dependency:`依据失效扣减`,messages:`消息清理`,retry:`批次重试`,consolidation:`模型整理`,persona:`persona 更新`,goals:`目标依据复核`},Vn={decayed:`衰减`,forgotten:`遗忘`,restored:`恢复`,deleted:`删除`,dependencies_weakened:`依据扣减`,messages_deleted:`清理消息`,batches_retried:`重试批次`,questions_expired:`询问到期结束`,checked:`检查项目`,skipped:`跳过`,failed:`失败`,merged:`合并`,conflicts:`待核对建议(矛盾或合并)`,dependencies_reviewed:`依赖复核建议`,persona_published:`persona 已发布`,persona_pending:`persona 待确认`,person_summary:`人物摘要处理`,goals_reviewed:`目标依据标注变化`,model_calls:`模型调用`},Hn={pinned:`已置顶`,revision_conflict:`修订冲突`,forgotten:`已遗忘`,deleted:`已删除`,no_longer_due:`尚未到期`,already_applied:`已经扣减`,dependency_removed:`依据关系已移除`,no_longer_eligible:`不再符合清理条件`,referenced:`仍有引用`,batch_state_changed:`批次状态已变化`,already_retried:`已自动重试过`,batch_date_changed:`批次日期已变化`,target_messages_cleared:`目标消息已清理`};function Un({summary:e}){return(0,x.jsx)(`dl`,{className:`maintenance-counts`,children:Object.entries(e).filter(([e])=>![`model_step_budget`,`backlog_trimmed`].includes(e)).map(([e,t])=>(0,x.jsxs)(`div`,{children:[(0,x.jsx)(`dt`,{children:Vn[e]||e}),(0,x.jsx)(`dd`,{children:t.count})]},e))})}function Wn({run:e}){return(0,x.jsxs)(`div`,{className:`lifecycle-stack`,children:[(0,x.jsxs)(`div`,{className:`actions`,children:[(0,x.jsx)(M,{tone:e.state===`completed`?`green`:`purple`,children:e.state===`completed`?`已完成`:`运行中`}),(0,x.jsx)(`span`,{children:zn[e.trigger]||e.trigger}),(0,x.jsxs)(`span`,{className:`muted`,children:[`#`,e.id]})]}),(0,x.jsxs)(`p`,{children:[`开始:`,w(e.created_at),` · 完成:`,e.finished_at?w(e.finished_at):`尚未完成`]})]})}function Gn({openMemory:e,initialRun:t}){let n=y(`/maintenance?limit=1&offset=0`,5e3),[r,i]=(0,l.useState)(!1),[a,o]=(0,l.useState)(0),s=y(r?`/maintenance?limit=10&offset=${a}`:null,5e3),[c,u]=(0,l.useState)(t||null),[d,f]=(0,l.useState)(!1),[p,m]=(0,l.useState)(!1),[h,b]=(0,l.useState)(``),S=n.data?.items[0];(0,l.useEffect)(()=>{let e=()=>{let e=location.hash;if(e.split(`?`)[0]!==`#/status`)return;let t=Number(new URLSearchParams(e.split(`?`)[1]||``).get(`run`));Number.isSafeInteger(t)&&t>0&&u(t)};return addEventListener(`hashchange`,e),()=>removeEventListener(`hashchange`,e)},[]);async function C(){if(!p){m(!0),b(``);try{let e=await g(`/maintenance`,_(`POST`,{}));f(!1),u(e.run_id),n.refresh(),s.refresh()}catch(e){b(v(e))}finally{m(!1)}}}return(0,x.jsxs)(`section`,{className:`panel maintenance-panel`,children:[(0,x.jsxs)(`div`,{className:`panel-heading`,children:[(0,x.jsx)(`h2`,{children:`梦境整理与记忆维护`}),(0,x.jsx)(`button`,{className:`secondary`,disabled:p,onClick:()=>f(!0),children:`手动维护`})]}),(h||n.error||s.error)&&(0,x.jsxs)(A,{error:!0,children:[h||n.error||s.error,(0,x.jsx)(`button`,{className:`text-button`,onClick:()=>{n.refresh(),s.refresh()},children:`刷新维护记录`})]}),d&&(0,x.jsxs)(`div`,{className:`delete-confirm`,children:[(0,x.jsx)(`p`,{children:`手动维护也计一次衰减,并执行遗忘、到期删除、消息清理和批次重试。还会按本次设置调用模型进行整理与 persona 与人物摘要更新,并复核目标依据。已有未完成的维护时继续该次运行。`}),(0,x.jsxs)(`div`,{className:`actions`,children:[(0,x.jsx)(`button`,{className:`primary`,disabled:p,onClick:()=>void C(),children:p?`正在请求…`:`确认运行维护`}),(0,x.jsx)(`button`,{className:`secondary`,disabled:p,onClick:()=>f(!1),children:`取消维护`})]})]}),!n.data&&!n.error&&(0,x.jsx)(`p`,{role:`status`,children:`正在读取维护记录…`}),n.data&&!S&&(0,x.jsx)(j,{title:`尚无维护记录`}),S&&(0,x.jsxs)(x.Fragment,{children:[(0,x.jsx)(`h3`,{className:`maintenance-subheading`,children:`最近一次维护`}),(0,x.jsx)(Wn,{run:S}),Object.keys(S.summary).length?(0,x.jsx)(Un,{summary:S.summary}):(0,x.jsx)(`p`,{className:`quiet`,children:`维护进行中,可在报告中查看当前数量。`}),(0,x.jsx)(`button`,{className:`text-button`,onClick:()=>u(S.id),children:`查看最近维护报告`})]}),(0,x.jsx)(`p`,{className:`fine-print`,children:`维护不中断接收和学习。报告逐项保留实际变化、模型建议和失败;检查及跳过数量按阶段和原因汇总。`}),(0,x.jsx)(`button`,{className:`text-button`,"aria-expanded":r,onClick:()=>i(e=>!e),children:`历史维护`}),r&&(0,x.jsxs)(`div`,{className:`lifecycle-stack`,children:[!s.data&&!s.error&&(0,x.jsx)(`p`,{role:`status`,children:`正在读取历史维护…`}),s.data?.items.length===0&&(0,x.jsx)(`p`,{className:`quiet`,children:`没有历史维护。`}),s.data?.items.map(e=>(0,x.jsxs)(`div`,{className:`maintenance-history-row`,children:[(0,x.jsxs)(`span`,{children:[`#`,e.id,` · `,w(e.created_at),` ·`,` `,zn[e.trigger]||e.trigger,` ·`,` `,e.state===`completed`?`已完成`:`运行中`]}),(0,x.jsxs)(`button`,{className:`text-button`,onClick:()=>u(e.id),children:[`查看报告 #`,e.id]})]},e.id)),s.data&&(0,x.jsx)(T,{total:s.data.total,limit:10,offset:a,change:o})]}),c!==null&&(0,x.jsx)(Kn,{id:c,close:()=>{u(null),t&&(location.hash=`#/status`)},openMemory:t=>{u(null),location.hash.startsWith(`#/status?`)&&window.history.replaceState(null,``,`#/status`),e(t)}},c)]})}function Kn({id:e,close:t,openMemory:n}){let[r,i]=(0,l.useState)(3e3),a=y(`/maintenance/${e}`,r),o=a.data;(0,l.useEffect)(()=>{o?.state===`completed`&&i(0)},[o?.state]);let s=o?[[`变化的记忆`,o.items.filter(e=>e.memory_id!=null&&e.outcome!==`failed`&&e.phase!==`consolidation`)],[`清理的消息`,o.items.filter(e=>e.outcome===`messages_deleted`)],[`重试的批次`,o.items.filter(e=>e.outcome===`batches_retried`)],[`失败项`,o.items.filter(e=>e.outcome===`failed`)]]:[];return(0,x.jsxs)(ee,{title:`维护报告 #${e}`,onClose:t,children:[a.error&&(0,x.jsxs)(A,{error:!0,children:[a.error,(0,x.jsx)(`button`,{className:`text-button`,onClick:a.refresh,children:`重新读取报告`})]}),!o&&!a.error&&(0,x.jsx)(`p`,{role:`status`,children:`正在读取维护报告…`}),o&&(0,x.jsxs)(`div`,{className:`lifecycle-stack`,children:[(0,x.jsx)(Wn,{run:o}),(0,x.jsxs)(`p`,{className:`muted`,children:[`角色时区:`,o.timezone,` · 当前阶段:`,o.state===`completed`?`已结束`:Object.values(Bn)[o.phase]||`生成报告`]}),(0,x.jsx)(Un,{summary:o.summary}),(0,x.jsxs)(`details`,{children:[(0,x.jsx)(`summary`,{children:`检查阶段与跳过原因`}),Object.entries(o.summary.checked?.by_phase||{}).map(([e,t])=>(0,x.jsxs)(`p`,{children:[Bn[e]||e,`:`,t]},e)),Object.entries(o.summary.skipped?.reasons||{}).map(([e,t])=>(0,x.jsxs)(`p`,{children:[Hn[e]||pe(e),`:`,t]},e))]}),(0,x.jsx)(Zt,{data:o,openMemory:n,refresh:a.refresh}),s.map(([t,r])=>(0,x.jsx)(qn,{name:t,items:r,openMemory:n},`${e}:${t}`))]})]})}function qn({name:e,items:t,openMemory:n}){let[r,i]=(0,l.useState)(0);return(0,x.jsxs)(`section`,{className:`detail-section`,"aria-label":e,children:[(0,x.jsxs)(`h3`,{children:[e,` `,(0,x.jsx)(M,{children:t.length})]}),!t.length&&(0,x.jsx)(`p`,{className:`quiet`,children:`暂无记录。`}),t.slice(r,r+30).map(e=>(0,x.jsxs)(`article`,{className:`maintenance-item`,children:[e.memory_id==null?(0,x.jsxs)(`strong`,{children:[e.phase===`messages`?`消息`:e.phase===`persona`?`persona 任务`:e.phase===`goals`?`目标`:`批次`,` `,`#`,e.object_id]}):(0,x.jsxs)(`button`,{className:`text-button`,onClick:()=>n(e.memory_id),children:[`记忆 #`,e.memory_id]}),(0,x.jsxs)(`p`,{children:[Bn[e.phase]||e.phase,` ·`,` `,Vn[e.outcome]||e.outcome]}),typeof e.details.before==`number`&&(0,x.jsxs)(`p`,{children:[`保留强度:`,e.details.before,` → `,e.details.after]}),e.details.transition&&(0,x.jsxs)(`p`,{children:[`状态变为:`,E(e.details.transition)]}),e.details.source_memory_id!==void 0&&(0,x.jsxs)(`p`,{children:[`失效依据:记忆 #`,e.details.source_memory_id]}),e.reason&&(0,x.jsx)(`p`,{className:`danger-text`,children:Hn[e.reason]||pe(e.reason)}),(0,x.jsx)(`small`,{children:w(e.created_at)})]},`${e.phase}:${e.item_key}`)),(0,x.jsx)(T,{total:t.length,offset:r,change:i})]})}var Jn={learning:`学习`,learning_repair:`学习 JSON 修正`,trial_reply:`试用回复`,trial_reply_repair:`回复 JSON 修正`,embedding:`向量生成`,image_understanding:`图片理解`,retrieval_query:`召回查询`,health_probe:`健康探测`,recall_judge:`召回判断`,goal_dedup_judge:`目标去重判断`,consolidation_merge:`整理合并判断`,consolidation_merge_repair:`整理合并 JSON 修正`,consolidation_conflict:`整理矛盾建议`,consolidation_conflict_repair:`整理矛盾 JSON 修正`,consolidation_dependency:`整理依赖复核`,consolidation_dependency_repair:`整理依赖 JSON 修正`,persona_generate:`persona 生成`,persona_check:`persona 检查`,connection_check:`连接测试`};function Yn({data:e,openMemory:t,initialQuery:n=``}){let r=y(`/catalog`),i=y(`/settings`,5e3);if(!e)return(0,x.jsx)(j,{title:`正在读取运行状态`});let a=Number(new URLSearchParams(n).get(`run`)),o=e.learning_latency_24h,s=e.models.find(e=>e.purpose===`image_understanding`),c=e.models.filter(e=>e.result_category!==`success`),l=new Map(r.data?.entries.map(e=>[e.id,e.name]));return(0,x.jsxs)(x.Fragment,{children:[(0,x.jsxs)(`div`,{className:`page-heading`,children:[(0,x.jsxs)(`div`,{children:[(0,x.jsx)(`p`,{className:`eyebrow`,children:`每一步,都看得见`}),(0,x.jsx)(`h1`,{children:`运行状态`}),(0,x.jsx)(`p`,{children:`服务、模型和学习队列的当前情况。`})]}),(0,x.jsx)(M,{tone:e.scheduler.running?`green`:`danger`,children:e.scheduler.running?`调度运行中`:`调度未运行`})]}),(0,x.jsxs)(`div`,{className:`stat-grid`,children:[(0,x.jsxs)(`section`,{className:`panel metric`,children:[(0,x.jsx)(`span`,{children:`待学习消息`}),(0,x.jsx)(`strong`,{children:e.entries.reduce((e,t)=>e+t.pending_count,0)}),(0,x.jsxs)(`small`,{children:[e.scheduler.max_concurrent,` 个学习批次可同时运行`]})]}),(0,x.jsxs)(`section`,{className:`panel metric`,children:[(0,x.jsx)(`span`,{children:`今日 token 用量`}),(0,x.jsx)(`strong`,{children:e.usage.today.tokens.toLocaleString()}),(0,x.jsxs)(`small`,{children:[e.usage.today.calls,` 次请求 ·`,` `,e.usage.today.reasoning_tokens.toLocaleString(),` 推理 token(包含在总量内)`]})]}),(0,x.jsxs)(`section`,{className:`panel metric`,children:[(0,x.jsx)(`span`,{children:`近 24 小时学习 P95`}),(0,x.jsx)(`strong`,{children:C(o.p95_ms)}),(0,x.jsxs)(`small`,{children:[o.count,` 次学习调用`]})]}),(0,x.jsxs)(`section`,{className:`panel metric`,children:[(0,x.jsx)(`span`,{children:`近 24 小时学习超时`}),(0,x.jsx)(`strong`,{children:o.timeouts}),(0,x.jsxs)(`small`,{children:[`共享学习总预算 `,(0,x.jsxs)(`b`,{children:[e.timeouts_seconds.learning,` 秒`]})]})]})]}),(0,x.jsx)(Gn,{openMemory:t,initialRun:Number.isSafeInteger(a)&&a>0?a:void 0}),(0,x.jsxs)(`div`,{className:`status-grid`,children:[(0,x.jsxs)(`section`,{className:`panel`,children:[(0,x.jsxs)(`div`,{className:`panel-heading`,children:[(0,x.jsx)(`h2`,{children:`模型用途`}),(0,x.jsx)(`span`,{className:`muted`,children:`实时健康状态`})]}),Object.entries(e.model_health).map(([t,n])=>(0,x.jsxs)(`div`,{className:`model-row`,children:[(0,x.jsxs)(`div`,{className:`model-title`,children:[(0,x.jsx)(`strong`,{children:Ee[t]||t}),(0,x.jsx)(M,{tone:n.state===`normal`?`green`:`danger`,children:D(n.state)})]}),(0,x.jsx)(`p`,{className:`muted`,children:t===`chat`?`用于学习与角色回复`:t===`recall_judge`?`判断相关候选能否回答当前请求`:t===`goal_dedup_judge`?`跨入口判断目标是否为同一件事`:t===`image_understanding`?`为没有宿主说明的图片补充理解;失败时使用占位继续学习`:`用于记忆向量与语义检索`}),t===`recall_judge`&&(0,x.jsxs)(x.Fragment,{children:[e.timeouts_seconds.recall_judge!=null&&(0,x.jsxs)(`p`,{children:[`总预算 `,e.timeouts_seconds.recall_judge,` 秒(含排队)`]}),i.error&&(0,x.jsxs)(`p`,{className:`muted`,children:[`判断开关状态暂时无法读取。`,(0,x.jsx)(`button`,{onClick:i.refresh,children:`重新读取`})]}),i.data?.recall_judge?.enabled===!1?(0,x.jsx)(`p`,{children:`召回判断已关闭,保留基础召回。`}):n.state!==`normal`&&(0,x.jsx)(`p`,{children:`召回判断暂停或退避时降级,保留仍有效的原召回。`}),n.retry_at&&(0,x.jsxs)(`p`,{children:[`退避截止:`,S(n.retry_at)]}),(0,x.jsx)(`a`,{href:`#/settings`,children:`管理召回判断设置`})]}),t===`goal_dedup_judge`&&(0,x.jsxs)(x.Fragment,{children:[i.error&&(0,x.jsxs)(`p`,{className:`muted`,children:[`判断设置暂时无法读取。`,(0,x.jsx)(`button`,{onClick:i.refresh,children:`重新读取`})]}),i.data?.goal_dedup_judge&&(0,x.jsxs)(`p`,{children:[`判断预算 `,i.data.goal_dedup_judge.budget_seconds,` `,`秒(含排队) · 并发`,` `,i.data.goal_dedup_judge.concurrency,` · 排队上限`,` `,i.data.goal_dedup_judge.queue_limit]}),i.data?.goal_dedup_judge?.enabled===!1?(0,x.jsx)(`p`,{children:`目标去重判断已关闭,使用确定性规则。`}):(0,x.jsx)(`p`,{children:`模型暂停、退避或判断失败时先保留目标,等待复核。管理员创建的目标不会自动合并。`}),n.retry_at&&(0,x.jsxs)(`p`,{children:[`退避截止:`,S(n.retry_at)]}),(0,x.jsx)(`a`,{href:`#/settings`,children:`管理目标去重判断设置`})]}),t===`image_understanding`&&(0,x.jsxs)(x.Fragment,{children:[(0,x.jsxs)(`p`,{children:[n.inherited?`沿用对话模型`:`使用独立图片理解配置`,` · 健康状态按图片理解用途独立记录。`]}),n.timeout_seconds!=null&&(0,x.jsxs)(`p`,{children:[`整批图片理解预算 `,n.timeout_seconds,` 秒(含排队与重试)`]}),n.retry_at&&(0,x.jsxs)(`p`,{children:[`退避截止:`,S(n.retry_at)]}),(0,x.jsx)(`a`,{href:`#/settings`,children:`管理图片理解设置`})]}),n.last_error&&(0,x.jsxs)(`p`,{className:`error-summary`,children:[`最近错误:`,n.last_error]}),n.next_probe_at&&(0,x.jsxs)(`small`,{children:[`下次探测:`,S(n.next_probe_at)]})]},t)),!e.model_health.image_understanding&&i.data?.models?.image_understanding?.enabled===!1&&(0,x.jsx)(`p`,{children:`图片理解未启用,没有宿主说明的图片以“未理解”占位继续学习。`}),(e.model_health.image_understanding||s||e.usage.by_purpose?.image_understanding)&&(0,x.jsxs)(`div`,{className:`model-row`,children:[(0,x.jsx)(`h3`,{children:`图片理解调用与用量`}),(0,x.jsx)(Rn,{value:e.usage.by_purpose?.image_understanding}),s&&(0,x.jsxs)(x.Fragment,{children:[(0,x.jsxs)(`p`,{children:[`最近调用:`,s.model,` ·`,` `,C(s.duration_ms),` ·`,` `,s.result_category===`success`?`成功`:`未成功`]}),(0,x.jsx)(`p`,{className:`muted`,children:S(s.created_at)})]})]})]}),(0,x.jsxs)(`section`,{className:`panel`,children:[(0,x.jsx)(`h2`,{children:`用量与学习`}),(0,x.jsxs)(`dl`,{className:`status-facts`,children:[(0,x.jsxs)(`div`,{children:[(0,x.jsx)(`dt`,{children:`每日 token 上限`}),(0,x.jsx)(`dd`,{children:e.budget.limit==null?`不限`:e.budget.limit.toLocaleString()})]}),(0,x.jsxs)(`div`,{children:[(0,x.jsx)(`dt`,{children:`未报告 token 的调用`}),(0,x.jsxs)(`dd`,{children:[e.usage.today.calls_without_usage,` 次`]})]}),(0,x.jsxs)(`div`,{children:[(0,x.jsx)(`dt`,{children:`学习耗时 P50 / 最大`}),(0,x.jsxs)(`dd`,{children:[C(o.p50_ms),` / `,C(o.max_ms)]})]}),(0,x.jsxs)(`div`,{children:[(0,x.jsx)(`dt`,{children:`缺少向量的记忆`}),(0,x.jsxs)(`dd`,{children:[e.missing_vectors,` 条`]})]}),(0,x.jsxs)(`div`,{children:[(0,x.jsx)(`dt`,{children:`记忆缺口`}),(0,x.jsxs)(`dd`,{children:[e.memory_gap_count,` 段`]})]})]}),(0,x.jsx)(`p`,{className:`fine-print`,children:`今日按角色时区统计。服务商未报告的用量和在途调用不计入已知总量;未配置价格,暂不估算费用。`})]})]}),(0,x.jsxs)(`section`,{className:`panel`,children:[(0,x.jsxs)(`div`,{className:`panel-heading`,children:[(0,x.jsx)(`h2`,{children:`入口队列`}),(0,x.jsx)(`span`,{className:`muted`,children:`接收、学习和形成记忆分别记录`})]}),e.entries.length?(0,x.jsx)(`div`,{className:`table-scroll`,children:(0,x.jsxs)(`table`,{children:[(0,x.jsx)(`thead`,{children:(0,x.jsxs)(`tr`,{children:[(0,x.jsx)(`th`,{children:`入口`}),(0,x.jsx)(`th`,{children:`待学习`}),(0,x.jsx)(`th`,{children:`当前批次`}),(0,x.jsx)(`th`,{children:`过滤与等待`}),(0,x.jsx)(`th`,{children:`最近结束的批次`}),(0,x.jsx)(`th`,{children:`未记住的消息`})]})}),(0,x.jsx)(`tbody`,{children:e.entries.map(e=>(0,x.jsxs)(`tr`,{children:[(0,x.jsx)(`td`,{children:l.get(e.entry_id)||e.entry_id}),(0,x.jsx)(`td`,{children:e.pending_count}),(0,x.jsx)(`td`,{children:k(e.current_batch)}),(0,x.jsx)(`td`,{children:(0,x.jsx)(st,{value:e.queue_wait})}),(0,x.jsx)(`td`,{children:k(e.latest_batch)}),(0,x.jsx)(`td`,{children:e.gap_message_count||0})]},e.entry_id))})]})}):(0,x.jsx)(`p`,{className:`quiet`,children:`暂无入口。`})]}),(0,x.jsxs)(`div`,{className:`status-grid`,children:[(0,x.jsxs)(`section`,{className:`panel`,children:[(0,x.jsx)(`h2`,{children:`最近学习调用`}),e.learning_calls_24h.length?(0,x.jsx)(`div`,{className:`call-list`,children:e.learning_calls_24h.slice(0,12).map((e,t)=>(0,x.jsxs)(`div`,{className:`call`,children:[(0,x.jsxs)(`span`,{children:[`批次 #`,e.batch_id,` · `,Jn[e.purpose]||e.purpose]}),(0,x.jsx)(`strong`,{children:C(e.duration_ms)}),e.timed_out?(0,x.jsx)(M,{tone:`danger`,children:`超时`}):e.error_summary?(0,x.jsx)(M,{tone:`danger`,children:`失败`}):(0,x.jsx)(M,{tone:`green`,children:`调用完成`}),e.error_summary&&(0,x.jsx)(`small`,{children:e.error_summary})]},t))}):(0,x.jsx)(`p`,{className:`quiet`,children:`近 24 小时没有学习调用。`}),(0,x.jsx)(`p`,{className:`fine-print`,children:`模型调用完成不等于批次成功;JSON 校验和记忆写入结果请看批次。`})]}),(0,x.jsxs)(`section`,{className:`panel`,children:[(0,x.jsx)(`h2`,{children:`错误摘要`}),(0,x.jsx)(`p`,{className:`muted section-help`,children:`来自现有状态接口的最近用途错误与调度错误`}),e.scheduler.last_error&&(0,x.jsx)(A,{error:!0,children:e.scheduler.last_error}),c.map(e=>(0,x.jsxs)(`div`,{className:`error-entry`,children:[(0,x.jsxs)(`strong`,{children:[Jn[e.purpose]||e.purpose,` · `,e.model]}),(0,x.jsx)(`p`,{children:e.error_summary||e.result_category}),(0,x.jsx)(`small`,{children:S(e.created_at)})]},e.purpose)),!c.length&&!e.scheduler.last_error&&(0,x.jsx)(`p`,{className:`quiet`,children:`最近的用途调用没有错误。`})]})]})]})}var Xn=e=>e.execution_state===`split`||e.state===`split`,Zn=e=>({full:`完整`,target_only:`仅目标`})[e||``]||`未记录`;function Qn(e,t){return Xn(e)?[`waiting`,`running`].includes(e.state)?`拆小重学中`:e.state===`succeeded`?`拆小重学已完成`:e.state===`split`?`历史请求被拒绝 · 已拆分`:`拆小重学结束 · 仍有缺口`:e.parent_batch_id&&[`waiting`,`running`].includes(e.state)?`拆小重学中`:e.state===`refused`&&(t===1||t===void 0&&e.parent_batch_id)?`该单消息的学习请求被拒绝`:e.relearning?`重新学习中`:k(e)}function $n(e){return new Set([...e.attempts.flatMap(e=>e.calls.map(e=>e.id)),...e.unassigned_calls.map(e=>e.id)]).size}function er({batch:e}){let t=e.split;if(!t)return null;let n=t.waiting_messages+t.running_messages,r=t.refused_messages+t.abandoned_messages,i=n+r+t.succeeded_messages,a=e.state===`succeeded`&&n===0&&r===0&&t.gap_count===0;return(0,x.jsxs)(`div`,{className:`split-progress`,children:[(0,x.jsxs)(`div`,{className:`split-metrics`,"aria-label":`子树消息进度`,children:[(0,x.jsxs)(`span`,{children:[e.parent_batch_id?`本子树目标`:`原目标`,` `,i,` 条`]}),(0,x.jsxs)(`span`,{children:[`已成功 `,t.succeeded_messages,` 条`]}),(0,x.jsxs)(`span`,{children:[`仍在补学 `,n,` 条`]}),(0,x.jsxs)(`span`,{children:[`最终被拒绝 `,t.refused_messages,` 条`]}),(0,x.jsxs)(`span`,{children:[`最终失败 `,r,` 条`]}),(0,x.jsxs)(`span`,{children:[`剩余缺口 `,t.gap_count,` 处`]})]}),(0,x.jsxs)(`p`,{children:[`批次树节点 `,t.node_count,` 个`]}),(0,x.jsx)(`p`,{className:`muted`,children:`消息结果仅统计末端子批次;最终失败包含拒绝与尝试用尽。父批次的历史拒绝单独保留。`}),a&&(0,x.jsx)(`p`,{className:`split-complete`,children:`全部成功,无缺口`}),!a&&(0,x.jsx)(`p`,{children:`缺口在对应子批次成功后消失;每处缺口可能包含多条消息。`})]})}function tr({batch:e,onOpen:t}){return e.parent_batch_id?(0,x.jsxs)(`div`,{className:`batch-links`,children:[e.root_batch_id&&(0,x.jsxs)(`button`,{className:`text-button`,onClick:()=>t(e.root_batch_id),"aria-label":`查看原始批次 #${e.root_batch_id}`,children:[`原始批次 #`,e.root_batch_id,` →`]}),e.parent_batch_id!==e.root_batch_id&&(0,x.jsxs)(`button`,{className:`text-button`,onClick:()=>t(e.parent_batch_id),"aria-label":`查看父批次 #${e.parent_batch_id}`,children:[`父批次 #`,e.parent_batch_id,` →`]}),(0,x.jsxs)(`span`,{children:[`路径 `,e.split_path]})]}):null}function nr(e){return e.count?`第 ${e.start+1}${e.count>1?`–${e.end}`:``} 条 · 共 ${e.count} 条`:`没有目标消息`}function rr({id:e}){let t=y(`/batches/${e}/tree?limit=1&offset=0`,3e3),n=t.data?.items.find(t=>t.id===e);return(0,x.jsxs)(x.Fragment,{children:[t.error&&(0,x.jsxs)(A,{error:!0,children:[t.error,(0,x.jsx)(`button`,{onClick:t.refresh,children:`重试读取目标范围`})]}),n&&(0,x.jsxs)(`p`,{children:[`原始目标范围:`,nr(n.target_range)]})]})}function ir({batch:e}){let t=Xn(e);return(0,x.jsxs)(`div`,{className:`batch-node-facts`,children:[(0,x.jsxs)(`p`,{children:[`上下文模式:`,Zn(e.context_mode)]}),t?(0,x.jsxs)(`p`,{children:[`拆分前已计 `,e.attempt_count??0,` 次尝试;执行已交给子批次。`]}):(0,x.jsxs)(`p`,{children:[`当前额度:已用 `,e.attempt_count??0,` / 4,剩余`,` `,e.attempts_remaining??Math.max(0,4-(e.attempt_count??0)),` `,`次`]}),e.state===`waiting`&&!t&&(0,x.jsx)(`p`,{children:`排队原因未提供;请结合入口和运行状态查看。`}),!t&&(0,x.jsxs)(`p`,{children:[`下次重试:`,e.next_retry_at?S(e.next_retry_at):`无定时时间`]})]})}function ar({id:e}){let t=y(`/batches/${e}`,3e3);return(0,x.jsxs)(x.Fragment,{children:[t.error&&(0,x.jsxs)(A,{error:!0,children:[t.error,(0,x.jsx)(`button`,{onClick:t.refresh,children:`重试读取历史`})]}),t.data?(0,x.jsxs)(x.Fragment,{children:[(0,x.jsxs)(`p`,{children:[`历史尝试编号:`,t.data.attempts.length?t.data.attempts.map(e=>e.number).join(`、`):`暂无已完成尝试`]}),(0,x.jsxs)(`p`,{children:[`本节点实际模型调用 `,$n(t.data),` 次`]}),(0,x.jsx)(`p`,{className:`muted`,children:`含学习与 JSON 修正请求及未归入尝试的调用;不等于本轮已用额度。`})]}):(0,x.jsx)(`p`,{children:`正在读取历史…`})]})}function or({id:e}){let[t,n]=(0,l.useState)(!1);return(0,x.jsxs)(`details`,{className:`node-history`,onToggle:e=>n(e.currentTarget.open),children:[(0,x.jsxs)(`summary`,{children:[`历史尝试与调用 · #`,e]}),t&&(0,x.jsx)(ar,{id:e})]})}function sr({id:e,onOpen:t}){let[n,r]=(0,l.useState)(!1),[i,a]=(0,l.useState)(0),[o,s]=(0,l.useState)([]),c=y(n?`/batches/${e}/tree?limit=100&offset=${i}`:null,3e3),u=c.data;return(0,x.jsxs)(`div`,{className:`split-tree`,children:[(0,x.jsx)(`button`,{className:`secondary`,"aria-expanded":n,"aria-controls":`split-tree-${e}`,"aria-label":`${n?`收起`:`展开`}拆分树 #${e}`,onClick:()=>r(!n),children:n?`收起拆分树`:`展开拆分树`}),n&&(0,x.jsxs)(`section`,{id:`split-tree-${e}`,"aria-label":`拆分树 #${e}`,children:[(0,x.jsx)(`p`,{className:`muted`,children:`范围按原始批次的目标消息顺序,从第 1 条开始;路径中 0 为左支、1 为右支。节点可展开历史尝试,或进入详情查看消息与调用。`}),c.error&&(0,x.jsxs)(A,{error:!0,children:[c.error,(0,x.jsx)(`button`,{onClick:c.refresh,children:`重试读取拆分树`})]}),u?(0,x.jsxs)(x.Fragment,{children:[(0,x.jsxs)(`p`,{children:[`显示节点 `,u.total?u.offset+1:0,`–`,u.offset+u.items.length,` / `,u.total]}),(0,x.jsx)(`ol`,{className:`batch-tree-nodes`,children:u.items.filter(e=>!o.some(t=>e.split_path!==t&&e.split_path?.startsWith(t))).map(n=>{let r=Xn(n),i=n.split_path||``,a=n.target_range,c=o.includes(i);return(0,x.jsxs)(`li`,{className:`batch-tree-node`,style:{"--tree-depth":Math.min(i.length,3)},children:[(0,x.jsxs)(`div`,{className:`section-title`,children:[(0,x.jsxs)(`strong`,{children:[`#`,n.id,` · `,r?`拆分父批次`:`子批次`]}),(0,x.jsx)(M,{children:Qn(n,a.count)})]}),(0,x.jsx)(`p`,{children:nr(a)}),(0,x.jsxs)(`p`,{children:[`路径 `,i||`根`]}),n.parent_batch_id&&(0,x.jsxs)(`button`,{className:`text-button`,"aria-label":`查看父批次 #${n.parent_batch_id}`,onClick:()=>t(n.parent_batch_id),children:[`父批次 #`,n.parent_batch_id,` →`]}),(0,x.jsx)(ir,{batch:n}),n.gap&&(0,x.jsxs)(`p`,{children:[`仍有记忆缺口`,[`waiting`,`running`].includes(n.state)?` · 拆小重学中`:``]}),n.relearn_blocked_reason&&!r&&(0,x.jsx)(`p`,{children:n.relearn_blocked_reason}),(0,x.jsxs)(`div`,{className:`batch-links`,children:[r&&(0,x.jsx)(`button`,{className:`text-button`,"aria-expanded":!c,"aria-label":`${c?`展开`:`折叠`}子批次 #${n.id}`,onClick:()=>s(c?o.filter(e=>e!==i):[...o,i]),children:c?`展开子批次`:`折叠子批次`}),n.id!==e&&(0,x.jsxs)(`button`,{className:`text-button`,"aria-label":`查看批次 #${n.id}`,onClick:()=>t(n.id),children:[`查看详情`,n.can_relearn&&!r?` / 重新学习`:``,` `,`→`]})]}),(0,x.jsx)(or,{id:n.id})]},n.id)})}),u.total>u.limit&&(0,x.jsxs)(`div`,{className:`learning-pagination tree-pagination`,children:[(0,x.jsx)(`button`,{disabled:u.offset===0,onClick:()=>{a(Math.max(0,u.offset-u.limit)),s([])},children:`上一页节点`}),(0,x.jsx)(`button`,{disabled:u.offset+u.items.length>=u.total,onClick:()=>{a(u.offset+u.limit),s([])},children:`下一页节点`})]}),(0,x.jsx)(`p`,{className:`muted`,children:`分页统计的是批次树节点;实际模型调用次数在各节点历史中单独显示。`})]}):(0,x.jsx)(`p`,{children:`正在读取拆分树…`})]})]})}var cr={topic:`常聊话题`,saying:`群内说法`,rule:`明确群规`};function lr({value:e}){return e.kind?(0,x.jsxs)(`div`,{className:`profile-item-content`,children:[(0,x.jsxs)(`p`,{children:[(0,x.jsx)(M,{children:cr[e.kind]||e.kind}),` `,(0,x.jsx)(`strong`,{children:e.label})]}),e.meaning&&(0,x.jsxs)(`p`,{children:[`含义:`,e.meaning]}),e.quotes?.map((e,t)=>(0,x.jsxs)(`blockquote`,{children:[e.text,(0,x.jsxs)(`footer`,{children:[`消息 #`,e.message_id,` ·`,` `,e.field===`content`?`消息正文`:e.field===`quote`?`转引原话`:`原话归属未记录`,` `,`· 原作者:`,e.speaker_name||e.speaker||`未记录`]})]},t))]}):(0,x.jsx)(`p`,{className:`entry-profile-content`,children:e.text})}var ur=e=>({small:`小`,medium:`中`,large:`大`})[e]||e,dr=e=>({periodic:`定期更新`,regenerate:`重新生成`,edit:`管理员编辑`,rollback:`回滚`})[e]||e,fr=e=>({published:`已发布`,candidate:`待确认`,rejected:`被拒绝`,failed:`失败`,conflict:`版本冲突`,skipped:`已跳过`})[e]||e,pr={message_cleaned:`原消息已清理`,message_changed:`原消息已变化`,memory_unavailable:`记忆已失效或删除`,memory_changed:`记忆已修改`,visibility_changed:`记忆可见范围已变化`,memory_sources_changed:`记忆来源已变化`,disabled:`本入口画像已关闭`,entry_kind:`私聊入口不生成画像`,daily_limit:`今天已经尝试过生成,请在下一个角色本地日期重试`,insufficient_messages:`首次生成需要最近 14 天至少 50 条消息`,no_evidence:`没有可用的本入口依据`,unconfigured:`尚未配置对话模型`,daily_token_limit:`达到今日 token 上限`,current_version_changed:`当前已发布版本已变化,结果未发布`,settings_changed:`画像设置已变化,结果未发布`,evidence_changed:`依据在生成期间发生变化,结果未发布`,interrupted:`服务中断,任务未完成`,unexpected_error:`生成失败,请查看运行状态`,timeout:`模型调用超时`,invalid_output:`模型输出格式无效,未发布`,invalid_sentence:`候选句格式无效`,too_many_deleted:`检查删除了超过 30% 的候选句`,too_short:`正文低于旧版摘要的最少字数要求`,too_long:`正文超过该版本的字数上限`,superseded:`已被新候选或编辑取代`,paused:`对话用途已暂停`,invalid_items:`候选条目列表格式无效`,invalid_item:`条目类型、名称或含义格式无效`,invalid_quotes:`每个模型条目需要 1—3 条引语`,invalid_quote:`引语格式无效或超过 100 字`,invalid_quote_reference:`引语消息编号不在本入口材料中`,quote_not_found:`引语不是消息正文或转引原话的连续片段`,invalid_evidence_reference:`含义引用的记忆依据无效`,duplicate_item:`条目重复`,account_identifier:`包含账号标识或长数字号段`,insufficient_topic_recurrence:`常聊话题缺少跨日期的独立消息正文依据`,too_many_items:`条目数超过 6 条(包含手写条目)`,token_budget:`超过画像的估算 token 预算`,empty:`检查后没有可保留内容`,administrator_rejected:`管理员拒绝`},mr=(e,t)=>e===`too_many_deleted`?t===2?`检查累计删除了超过 50% 的模型条目`:t===1?`检查删除了超过 30% 的候选句`:`检查删除比例超出该版本上限,请查看版本检查记录`:pr[e]||e,hr=e=>e.memory_id?`记忆 #${e.memory_id} · 修订 ${e.revision}`:`消息 #${e.message_id}`,gr={evidence_budget_exceeded:`必要依据超出单次检查预算,暂不能完成这次检查`,basis_changed:`依据已变化,需要重新检查`,reject:`本次自动检查未通过`,invalid_output:`模型返回的检查结果格式无效,未据此准入`,timeout:`本次模型调用超时,已保留此前处理结果`,rule_scope_not_established:`尚未建立这条群规的适用范围依据`,bundle_rejected:`同组检查已有页面未通过,后续页已取消`,stale_check:`检查所用的正文或依据已变化,需要重新检查`,call_budget:`本轮调用额度已用完,剩余工作留待后续处理`,purged:`相关内容已彻底清除,本项停止处理`,merged_basis:`合并后的依据需要重新检查`,no_new_evidence:`没有新依据,本次跳过;不会重复增加保留强度。`,snapshot_changed:`依据或画像已变化,本次结果未写入。`},_r=e=>e.startsWith(`unbound_entities:`)?`相关对象缺少绑定依据:${e.slice(17).split(`,`).join(`、`)}`:gr[e]||mr(e),vr=e=>({manual:`人工条目`,candidate:`候选`,pending:`等待自动检查`,accepted:`已准入`,rejected:`检查未通过`})[e]||e;function yr({base:e,openMemory:t}){let[n,r]=(0,l.useState)(0),i=y(`${e}/versions?limit=10&offset=${n}`),[a,o]=(0,l.useState)(null),s=(0,l.useRef)(null),c=y(a===null?null:`${e}/versions/${a}`),[u,d]=(0,l.useState)(`1`),[f,p]=(0,l.useState)(`2`),[m,h]=(0,l.useState)(null),g=y(m),_=e=>{o(e),requestAnimationFrame(()=>s.current?.scrollIntoView?.({behavior:`smooth`,block:`start`}))};return(0,x.jsxs)(x.Fragment,{children:[(0,x.jsxs)(`section`,{className:`entry-profile-section`,"aria-label":`历史(旧形式)`,children:[(0,x.jsx)(`h3`,{children:`历史(旧形式)`}),(0,x.jsx)(`p`,{className:`muted`,children:`旧版本只读,保留原有依据与检查记录;不能回滚,也不再确认或拒绝候选。这里的版本号与宿主投影版本无关。`}),i.error&&(0,x.jsxs)(A,{error:!0,children:[i.error,(0,x.jsx)(`button`,{onClick:i.refresh,children:`重试历史`})]}),i.data?(0,x.jsxs)(x.Fragment,{children:[(0,x.jsx)(`ul`,{className:`entry-profile-history`,children:i.data.items.map(e=>(0,x.jsxs)(`li`,{children:[(0,x.jsxs)(`div`,{children:[(0,x.jsxs)(`strong`,{children:[`v`,e.version]}),` · `,fr(e.status),` ·`,` `,dr(e.source),` · 变化`,ur(e.change_degree),(0,x.jsx)(`p`,{className:`muted`,children:w(e.generated_at)})]}),(0,x.jsxs)(`button`,{className:`secondary`,onClick:()=>_(e.version),children:[`查看旧版 v`,e.version]})]},e.version))}),!i.data.items.length&&(0,x.jsx)(`p`,{children:`还没有版本记录。`}),(0,x.jsx)(T,{total:i.data.total,offset:n,limit:10,change:r})]}):(0,x.jsx)(`p`,{children:`正在读取历史…`}),(0,x.jsxs)(`form`,{className:`entry-profile-compare`,onSubmit:t=>{t.preventDefault(),h(`${e}/diff?before_version=${Number(u)}&after_version=${Number(f)}`),g.refresh()},children:[(0,x.jsxs)(`label`,{children:[`对比起始版本`,(0,x.jsx)(`input`,{type:`number`,required:!0,min:1,max:2**53-1,value:u,onChange:e=>d(e.target.value)})]}),(0,x.jsxs)(`label`,{children:[`对比目标版本`,(0,x.jsx)(`input`,{type:`number`,required:!0,min:1,max:2**53-1,value:f,onChange:e=>p(e.target.value)})]}),(0,x.jsx)(`button`,{children:`逐句对比`})]}),g.error&&(0,x.jsx)(A,{error:!0,children:g.error}),g.data&&(0,x.jsxs)(`div`,{"aria-label":`逐句差异`,children:[!g.data.changed&&(0,x.jsx)(`p`,{children:`两版内容与依据相同。`}),g.data.sentences.map((e,t)=>(0,x.jsxs)(`div`,{className:`entry-profile-diff ${e.op}`,children:[(0,x.jsx)(`strong`,{children:{equal:JSON.stringify(e.before)===JSON.stringify(e.after)?`保留`:`依据或手写标记变化`,replace:`改写`,insert:`新增`,delete:`删除`}[e.op]||e.op}),e.op===`equal`&&JSON.stringify(e.before)===JSON.stringify(e.after)?e.after.map((e,t)=>(0,x.jsx)(xr,{value:e},t)):(0,x.jsxs)(`div`,{className:`entry-profile-diff-columns`,children:[(0,x.jsxs)(`div`,{children:[(0,x.jsx)(`h4`,{children:`起始版本`}),e.before.map((e,t)=>(0,x.jsx)(xr,{value:e},t))]}),(0,x.jsxs)(`div`,{children:[(0,x.jsx)(`h4`,{children:`目标版本`}),e.after.map((e,t)=>(0,x.jsx)(xr,{value:e},t))]})]})]},t))]})]}),a!==null&&(0,x.jsxs)(`section`,{ref:s,className:`entry-profile-section`,"aria-label":`旧版详情`,children:[(0,x.jsxs)(`h3`,{children:[`旧版 v`,a,` 的依据与检查`]}),(0,x.jsx)(M,{children:`只读历史`}),c.error?(0,x.jsxs)(A,{error:!0,children:[c.error,(0,x.jsx)(`button`,{onClick:c.refresh,children:`重试版本详情`})]}):c.data?(0,x.jsx)(br,{value:c.data,openMemory:t}):(0,x.jsx)(`p`,{children:`正在读取版本…`})]})]})}function br({value:e,openMemory:t}){let n=e.checks,r=n.model,i=n.format_version||1,a=n.reason!==`invalid_output`&&r&&typeof r.reason==`string`&&Array.isArray(r.sentences)&&r.sentences.length===n.checked_sentences?.length&&r.sentences.every((e,t)=>e&&typeof e==`object`&&e.index===t+1&&typeof e.supported==`boolean`&&typeof e.scene_qualified==`boolean`&&typeof e.reason==`string`&&Array.isArray(e.violations)&&e.violations.every(e=>typeof e==`string`));return(0,x.jsxs)(x.Fragment,{children:[(0,x.jsxs)(`p`,{children:[fr(e.status),` · `,dr(e.source),` · 变化`,ur(e.change_degree),` · `,w(e.generated_at)]}),i===1&&(0,x.jsx)(M,{children:`旧版摘要`}),e.redacted&&(0,x.jsxs)(A,{children:[`此旧版本的相关内容已彻底清除。`,e.redaction_reason&&`原因:${e.redaction_reason}`]}),e.possibly_stale&&(0,x.jsx)(A,{children:`此版本依据可能过时。`}),(0,x.jsx)(`ol`,{className:`entry-profile-sentences`,children:e.sentences.map((e,n)=>(0,x.jsxs)(`li`,{children:[(0,x.jsx)(lr,{value:e}),(0,x.jsx)(M,{children:e.admin_written?`管理员手写 · 受保护`:`模型生成`}),e.admin_written&&!e.basis.length&&(0,x.jsx)(`p`,{className:`muted`,children:`管理员直接提供,无模型依据。`}),e.basis.map((e,n)=>(0,x.jsxs)(`div`,{className:`entry-profile-evidence`,children:[e.memory_id?(0,x.jsx)(`button`,{className:`text-button`,onClick:()=>t(e.memory_id),children:hr(e)}):(0,x.jsx)(`strong`,{children:hr(e)}),e.evidence?(0,x.jsxs)(x.Fragment,{children:[(0,x.jsx)(`p`,{children:e.evidence.content}),e.evidence.occurred_at&&(0,x.jsx)(`small`,{children:w(e.evidence.occurred_at)}),e.evidence.source_messages?.map(e=>(0,x.jsxs)(`blockquote`,{children:[e.content,(0,x.jsxs)(`footer`,{children:[`本入口原消息 #`,e.message_id,` ·`,` `,w(e.occurred_at)]})]},e.message_id))]}):(0,x.jsx)(`p`,{children:`此版本未保存来源摘录,可按依据编号核对历史。`}),e.memory&&(0,x.jsxs)(`small`,{children:[`当前修订 `,e.memory.revision,` ·`,` `,{active:`有效`,forgotten:`已遗忘`,deleted:`已删除`}[e.memory.lifecycle]||e.memory.lifecycle]})]},n))]},n))}),(0,x.jsxs)(`h4`,{children:[`检查结果:`,n.passed?`通过`:`未通过`]}),n.reason&&(0,x.jsx)(`p`,{children:mr(n.reason,i)}),n.quote_validation&&(0,x.jsxs)(`p`,{children:[`引用原话核对:`,n.quote_validation.passed,` /`,` `,n.quote_validation.total,` `,`条通过(含被删条目的引语;不代表语义支持)`]}),!!n.quote_validation_items?.length&&(0,x.jsxs)(`details`,{children:[(0,x.jsx)(`summary`,{children:`逐条引用核对`}),(0,x.jsx)(`ul`,{children:n.quote_validation_items.map(e=>(0,x.jsxs)(`li`,{children:[`候选第 `,e.index,` 项:`,e.passed,` / `,e.total,` 条原话通过`,e.quotes.filter(e=>!e.passed).map(e=>(0,x.jsxs)(`p`,{children:[`引语 `,e.index,`:`,mr(e.reason||`invalid_quote`,i)]},e.index))]},e.index))})]}),n.deterministic?.characters!=null&&(0,x.jsxs)(`p`,{children:[`正文 `,n.deterministic.characters,` 字 ·`,` `,n.deterministic.tokens??`未记录`,` 估算 token`]}),(0,x.jsxs)(`p`,{children:[`确定性检查:`,n.deterministic?.passed?`通过`:`存在删句或错误,见下方记录`]}),n.errors?.length?(0,x.jsx)(`ul`,{children:n.errors.map((e,t)=>(0,x.jsx)(`li`,{children:mr(e,i)},t))}):null,a&&r?(0,x.jsxs)(x.Fragment,{children:[(0,x.jsxs)(`p`,{children:[`模型判断:`,r.reason]}),(0,x.jsx)(`ol`,{children:r.sentences.map((e,t)=>(0,x.jsxs)(`li`,{children:[(0,x.jsx)(`p`,{children:n.checked_sentences?.[t]?.text}),(0,x.jsxs)(`p`,{children:[`依据`,e.supported?`支持`:`不支持`,` · 场景限定`,e.scene_qualified?`通过`:`未通过`,` ·`,` `,n.checked_sentences?.[t]?.meaning&&typeof e.meaning_supported==`boolean`?`含义${e.meaning_supported?`有依据`:`无依据,已移除`} · `:``,e.violations.length?e.violations.join(`、`):`未发现禁止内容`]}),(0,x.jsx)(`p`,{children:e.reason})]},e.index))})]}):(0,x.jsx)(`p`,{children:r?`模型检查输出格式无效,本版未通过检查。`:n.admin_edit?`管理员编辑只做确定性检查。`:`本版未执行模型检查。`}),!!n.removed_meanings?.length&&(0,x.jsxs)(x.Fragment,{children:[(0,x.jsx)(`h4`,{children:`已删含义记录`}),(0,x.jsx)(`p`,{children:`说法本身可以保留;没有含义不表示生成失败。`}),(0,x.jsx)(`ul`,{children:n.removed_meanings.map((e,t)=>(0,x.jsxs)(`li`,{children:[`候选第 `,e.index,` 项:`,e.meaning,(0,x.jsx)(`p`,{children:mr(e.reason,i)})]},t))})]}),!!n.admin_warnings?.length&&(0,x.jsxs)(x.Fragment,{children:[(0,x.jsx)(`h4`,{children:`手写条目检查提醒`}),(0,x.jsx)(`ul`,{children:n.admin_warnings.map((e,t)=>(0,x.jsxs)(`li`,{children:[`第 `,e.index,` 项:`,e.result.violations.join(`、`),` ·`,` `,e.result.reason]},t))})]}),!!n.deleted_sentences?.length&&(0,x.jsxs)(x.Fragment,{children:[(0,x.jsx)(`h4`,{children:i===2?`已删条目记录`:`已删句记录`}),(0,x.jsx)(`ul`,{children:n.deleted_sentences.map((e,t)=>(0,x.jsxs)(`li`,{children:[(0,x.jsx)(`p`,{children:typeof e.sentence?.text==`string`?e.sentence.text:`候选第 ${e.index} 项`}),(0,x.jsxs)(`p`,{children:[e.stage===`deterministic`?`引用与格式核对:`:e.stage===`model`?`模型检查:`:``,mr(e.reason,i)]})]},t))})]})]})}function xr({value:e}){return(0,x.jsxs)(`div`,{children:[(0,x.jsx)(lr,{value:e}),(0,x.jsxs)(`small`,{children:[e.author===`admin`?`管理员手写`:`模型生成`,e.basis?.length?` · ${e.basis.map(hr).join(`;`)}`:``]})]})}var Sr={topic:`话题`,saying:`说法`,rule:`群规`},Cr=e=>e?Sr[e]:`管理员说明`;function wr({entry:e,blocked:t,busy:n,write:r,cancel:i,created:a}){let[o,s]=(0,l.useState)(`rule`),[c,u]=(0,l.useState)(``),[d,f]=(0,l.useState)(``);return(0,x.jsxs)(`form`,{className:`profile-memory-card`,"aria-label":`人工新建画像记忆`,onSubmit:async n=>{if(n.preventDefault(),!(t||!c.trim()||Array.from(c).length>1e3)){f(``);try{await r(`/memories`,`POST`,{kind:`入口画像`,owner_entry_id:e.id,category:o,content:c}),a()}catch(e){f(v(e))}}},children:[(0,x.jsx)(`h4`,{children:`人工新建画像记忆`}),(0,x.jsxs)(`p`,{children:[`仅用于入口“`,e.name,`”。人工内容无需模型依据。`]}),d&&(0,x.jsx)(A,{error:!0,children:d}),(0,x.jsxs)(`label`,{children:[`子类`,(0,x.jsxs)(`select`,{"aria-label":`子类`,value:o||``,disabled:n,onChange:e=>s(e.target.value||null),children:[Object.entries(Sr).map(([e,t])=>(0,x.jsx)(`option`,{value:e,children:t},e)),(0,x.jsx)(`option`,{value:``,children:`管理员说明`})]})]}),(0,x.jsxs)(`label`,{children:[`新画像正文`,(0,x.jsx)(`textarea`,{"aria-label":`新画像正文`,rows:4,maxLength:1e3,value:c,disabled:n,onChange:e=>u(e.target.value)})]}),(0,x.jsxs)(`p`,{className:`fine-print`,children:[Array.from(c).length,` / 1000 字`]}),(0,x.jsxs)(`div`,{className:`button-row`,children:[(0,x.jsx)(`button`,{disabled:t||!c.trim()||Array.from(c).length>1e3,children:`创建画像记忆`}),(0,x.jsx)(`button`,{type:`button`,className:`secondary`,disabled:n,onClick:i,children:`取消新建`})]})]})}function Tr({id:e,revision:t,message:n,openMemory:r,speakerNames:i,pendingSources:a}){let o=y(`/memories/${e}`),s=(0,l.useRef)(null);return(0,l.useEffect)(()=>{o.refresh()},[t,o.refresh]),(0,l.useEffect)(()=>{o.data&&n!==null&&(s.current?.scrollIntoView?.({behavior:`smooth`,block:`start`}),s.current?.focus({preventScroll:!0}))},[o.data,n]),(0,x.jsxs)(`div`,{className:`profile-memory-evidence`,children:[o.error&&(0,x.jsxs)(A,{error:!0,children:[o.error,(0,x.jsx)(`button`,{onClick:o.refresh,children:`重试来源与修订`})]}),o.data?(0,x.jsxs)(x.Fragment,{children:[(0,x.jsx)(`h4`,{children:`来源消息与前后文`}),!o.data.sources.length&&(0,x.jsx)(`p`,{children:`没有保存的来源消息。`}),o.data.sources.map(e=>(0,x.jsxs)(`section`,{ref:e.message_id===n?s:null,tabIndex:-1,className:e.message_id===n?`profile-source-target`:``,"aria-label":e.message_id?`来源消息 #${e.message_id}`:`来源 #${e.id}`,children:[e.message_id&&(0,x.jsxs)(`h5`,{children:[`消息 #`,e.message_id]}),a.some(t=>t.message.position===e.message_id)&&(0,x.jsx)(A,{children:`该消息含待审引文,请以上方已审/待审分区为准`}),(0,x.jsx)(ln,{source:e,openMemory:r,initiallyOpen:e.message_id===n,speakerNames:i},`${e.id}:${e.message_id===n}`)]},e.id)),n!==null&&!o.data.sources.some(e=>e.message_id===n)&&(0,x.jsx)(A,{children:`该来源已变化或不可用,请刷新画像核对。`}),(0,x.jsxs)(`h4`,{children:[`修订历史 · `,o.data.revisions.length]}),o.data.revisions.length?o.data.revisions.map(e=>(0,x.jsx)(un,{revision:e},e.id)):(0,x.jsx)(`p`,{children:`还没有修订记录。`})]}):!o.error&&(0,x.jsx)(`p`,{children:`正在读取来源与修订…`})]})}function Er({memory:e,speakerNames:t,displayed:n,blocked:r,busy:i,write:a,refresh:o,openMemory:s}){let[c,u]=(0,l.useState)(null),[d,p]=(0,l.useState)(null),[m,h]=(0,l.useState)(!1),[g,_]=(0,l.useState)(``),[y,b]=(0,l.useState)(``),[S,C]=(0,l.useState)(!1),[T,D]=(0,l.useState)(null),O=!!c&&c.revision!==e.revision,k=e.lifecycle===`deleted`;function j(e){e instanceof f&&e.status===409?(h(!0),_(`这条画像记忆已变化,草稿已保留。请核对最新正文与修订后继续。`)):e instanceof f&&e.status===404?(h(!0),_(`这条画像记忆已不可用,草稿已保留。请复制需要保留的内容后关闭。`)):_(v(e))}async function ee(t,n,i,o){if(!r){_(``),b(``);try{await a(`/memories/${e.memory_id}${n}`,t,i),u(null),p(null),h(!1),b(o)}catch(e){j(e)}}}return(0,x.jsxs)(`article`,{id:`profile-memory-${e.memory_id}`,className:`profile-memory-card`,"aria-label":`画像记忆 #${e.memory_id}`,children:[(0,x.jsxs)(`div`,{className:`detail-badges`,children:[(0,x.jsx)(M,{tone:`purple`,children:Cr(e.category)}),(0,x.jsx)(M,{children:e.author===`admin`?`人工`:`模型`}),(0,x.jsx)(M,{children:vr(e.admission_state)}),e.manual_protected&&(0,x.jsx)(M,{children:`人工保护`}),(0,x.jsx)(M,{tone:k?`danger`:``,children:E(e.lifecycle)}),(0,x.jsxs)(`span`,{className:`muted`,children:[`#`,e.memory_id,` · 修订 `,e.revision]})]}),(0,x.jsx)(`p`,{className:`entry-profile-content`,children:e.body}),e.admission_state===`candidate`&&(0,x.jsx)(A,{children:`还在等待更多独立讨论,暂不给宿主看`}),e.admission_state===`pending`&&(0,x.jsx)(A,{children:`正在排队或自动检查,全部检查页通过前暂不给宿主看。`}),e.admission_state===`rejected`&&(0,x.jsx)(A,{children:`自动检查未通过,暂不给宿主看;后续由系统继续处理新依据。`}),e.admission_reason&&(0,x.jsxs)(`div`,{children:[(0,x.jsx)(`p`,{children:_r(e.admission_reason)}),e.admission_state===`manual`&&(0,x.jsx)(`p`,{className:`fine-print`,children:`这是自动依据检查记录,不影响人工正文的使用。`}),e.admission_state===`accepted`&&(0,x.jsx)(`p`,{className:`fine-print`,children:`后续检查记录不代表已准入正文被撤下,是否展示请看宿主投影。`})]}),(0,x.jsxs)(`dl`,{className:`profile-memory-metadata`,children:[(0,x.jsxs)(`div`,{children:[(0,x.jsx)(`dt`,{children:`保留强度`}),(0,x.jsxs)(`dd`,{children:[e.retention,e.pinned?` · 已置顶`:``]})]}),(0,x.jsxs)(`div`,{children:[(0,x.jsx)(`dt`,{children:`最近确认时间`}),(0,x.jsx)(`dd`,{children:e.last_confirmed_at?w(e.last_confirmed_at):`暂无确认记录`})]}),(0,x.jsxs)(`div`,{children:[(0,x.jsx)(`dt`,{children:`独立日期`}),(0,x.jsxs)(`dd`,{children:[e.independent_days,` 天`]})]}),(0,x.jsxs)(`div`,{children:[(0,x.jsx)(`dt`,{children:`独立依据`}),(0,x.jsxs)(`dd`,{children:[e.independent_uses,` 次`]})]}),(0,x.jsxs)(`div`,{children:[(0,x.jsx)(`dt`,{children:`宿主投影`}),(0,x.jsx)(`dd`,{children:n?`已加入`:e.usable?`当前未加入`:`当前不可用`})]})]}),(0,x.jsx)(`p`,{className:`fine-print`,children:`独立日期按角色时区统计;独立依据已排除转引、复述等重复讨论,不等于消息总数。`}),(0,x.jsxs)(`div`,{"aria-label":`依据原话`,children:[(0,x.jsxs)(`h4`,{children:[`已审依据 · `,e.sources.length,` 条`]}),e.sources.map((e,n)=>(0,x.jsxs)(`blockquote`,{children:[(0,x.jsx)(`p`,{children:e.excerpt}),(0,x.jsxs)(`footer`,{children:[e.field===`quote`?`转引原话`:`消息正文`,` · 原作者:`,t[e.speaker]||e.speaker,` ·`,` `,w(e.message.occurred_at)]}),(0,x.jsxs)(`button`,{className:`text-button`,disabled:i,onClick:()=>{D(e.message.position),C(!0)},children:[`跳转来源消息 #`,e.message.position]})]},`${e.ref}:${n}`)),!e.sources.length&&(0,x.jsx)(`p`,{className:`muted`,children:e.author===`admin`?`管理员手写,无需模型依据。`:`当前没有可用的绑定原话,可在来源与修订历史中核对。`})]}),!!e.pending_sources.length&&(0,x.jsxs)(`section`,{"aria-label":`待审来源`,className:`profile-pending-sources`,children:[(0,x.jsxs)(`h4`,{children:[`待审来源 · `,e.pending_sources.length,` 条`]}),(0,x.jsxs)(`p`,{className:`fine-print`,children:[`这些引文尚未完成自动准入检查,不作为已审依据。`,e.admission_state===`manual`?`人工正文照常可用,无需等待这些引文。`:`通过检查后才可能成为可展示画像的依据。`]}),e.pending_sources.map((e,n)=>(0,x.jsxs)(`blockquote`,{children:[(0,x.jsx)(`p`,{children:e.excerpt}),(0,x.jsxs)(`footer`,{children:[e.field===`quote`?`转引原话`:`消息正文`,` · 原作者:`,t[e.speaker]||e.speaker,` ·`,` `,w(e.message.occurred_at)]}),(0,x.jsxs)(`button`,{className:`text-button`,disabled:i,onClick:()=>{D(e.message.position),C(!0)},children:[`跳转来源消息 #`,e.message.position]})]},`${e.ref}:${n}`))]}),g&&(0,x.jsxs)(A,{error:!0,children:[g,(0,x.jsx)(`button`,{className:`text-button`,disabled:i,onClick:()=>{o(),!c&&d===null&&(h(!1),_(``))},children:`刷新画像记忆`})]}),y&&(0,x.jsx)(A,{children:y}),c&&(0,x.jsxs)(`form`,{onSubmit:e=>{e.preventDefault(),!r&&!m&&!O&&!k&&c.content.trim()&&ee(`PATCH`,``,{expected_revision:c.revision,content:c.content},`修改已保存。`)},children:[(0,x.jsxs)(`label`,{children:[`画像正文`,(0,x.jsx)(`textarea`,{"aria-label":`画像正文`,value:c.content,disabled:i,rows:4,maxLength:1e3,onChange:e=>u({...c,content:e.target.value})})]}),(0,x.jsxs)(`p`,{className:`fine-print`,children:[`草稿基于修订 `,c.revision,` · `,Array.from(c.content).length,` / 1000 字`]}),(O||m)&&(0,x.jsxs)(A,{children:[`草稿未被替换。上方显示当前修订 `,e.revision,` `,`的正文,请核对后继续。`,(0,x.jsx)(`button`,{type:`button`,disabled:r||k||m&&!O,onClick:()=>{u({...c,revision:e.revision}),h(!1),_(``)},children:`已核对最新修订,保留草稿继续`})]}),(0,x.jsxs)(`div`,{className:`button-row`,children:[(0,x.jsx)(`button`,{disabled:r||m||O||k||!c.content.trim()||Array.from(c.content).length>1e3,children:`保存修改`}),(0,x.jsx)(`button`,{className:`secondary`,type:`button`,disabled:i,onClick:()=>{u(null),h(!1),_(``)},children:`取消编辑`})]})]}),d!==null&&(0,x.jsxs)(`div`,{className:`delete-confirm`,children:[(0,x.jsx)(`p`,{children:`确认删除这条画像记忆?该标识将永久撤销,不能恢复;来源和修订历史保留。`}),d!==e.revision&&(0,x.jsx)(A,{children:`这条记忆已变化,请取消并核对最新正文后重新操作。`}),(0,x.jsxs)(`div`,{className:`button-row`,children:[(0,x.jsx)(`button`,{className:`danger-button`,disabled:r||m||k||d!==e.revision,onClick:()=>void ee(`DELETE`,``,{expected_revision:d},`画像记忆已删除,历史仍保留。`),children:`确认删除`}),(0,x.jsx)(`button`,{className:`secondary`,disabled:i,onClick:()=>{p(null),h(!1),_(``)},children:`取消删除`})]})]}),k&&(0,x.jsx)(`p`,{className:`muted`,children:`已删除的记忆标识不可恢复。需要重新手写时,请使用人工新建。`}),(0,x.jsxs)(`div`,{className:`button-row`,children:[!k&&(0,x.jsx)(`button`,{className:`secondary`,disabled:r||!!c||d!==null,onClick:()=>{u({content:e.body,revision:e.revision}),h(!1),_(``),b(``)},children:`编辑`}),!k&&(0,x.jsx)(`button`,{className:`text-button danger-text`,disabled:r||!!c||d!==null,onClick:()=>{p(e.revision),h(!1),_(``),b(``)},children:`删除`}),e.lifecycle===`forgotten`&&(0,x.jsx)(`button`,{className:`secondary`,disabled:r||!!c||d!==null||m,onClick:()=>void ee(`POST`,`/restore`,{expected_revision:e.revision},`画像记忆已恢复。`),children:`恢复`}),(0,x.jsx)(`button`,{className:`text-button`,"aria-expanded":S,disabled:i,onClick:()=>{C(!S),D(null)},children:S?`收起来源与修订历史`:`来源与修订历史`}),(0,x.jsx)(`button`,{className:`text-button`,disabled:i||!!c,onClick:()=>s(e.memory_id),children:`打开记忆详情`})]}),S&&(0,x.jsx)(Tr,{id:e.memory_id,speakerNames:t,revision:e.revision,pendingSources:e.pending_sources,message:T,openMemory:s})]})}function Dr({progress:e}){return e?(0,x.jsxs)(`section`,{"aria-label":`当前入口进度与积压`,children:[(0,x.jsx)(`h4`,{children:`当前入口进度与积压`}),(0,x.jsxs)(`dl`,{className:`profile-progress-grid`,children:[(0,x.jsxs)(`div`,{children:[(0,x.jsx)(`dt`,{children:`已完成观察分块`}),(0,x.jsxs)(`dd`,{children:[e.completed_chunks,` / `,e.observation_chunks,` 块`]})]}),(0,x.jsxs)(`div`,{children:[(0,x.jsx)(`dt`,{children:`待检查`}),(0,x.jsxs)(`dd`,{children:[e.pending_checks,` 页`]})]}),(0,x.jsxs)(`div`,{children:[(0,x.jsx)(`dt`,{children:`检查失败`}),(0,x.jsxs)(`dd`,{children:[e.failed_checks,` 页`]})]}),(0,x.jsxs)(`div`,{children:[(0,x.jsx)(`dt`,{children:`检查未通过`}),(0,x.jsxs)(`dd`,{children:[e.rejected_checks,` 页`]})]}),(0,x.jsxs)(`div`,{children:[(0,x.jsx)(`dt`,{children:`未准入画像`}),(0,x.jsxs)(`dd`,{children:[e.waiting_candidates,` 条`]})]}),(0,x.jsxs)(`div`,{children:[(0,x.jsx)(`dt`,{children:`待扫描`}),(0,x.jsxs)(`dd`,{children:[e.unscanned_messages,` 条消息`]})]}),(0,x.jsxs)(`div`,{children:[(0,x.jsx)(`dt`,{children:`观察缺口`}),(0,x.jsxs)(`dd`,{children:[e.observation_gaps,` 块`]})]}),(0,x.jsxs)(`div`,{children:[(0,x.jsx)(`dt`,{children:`最早积压时间`}),(0,x.jsx)(`dd`,{children:e.oldest_pending_at?w(e.oldest_pending_at):`无`})]}),(0,x.jsxs)(`div`,{children:[(0,x.jsx)(`dt`,{children:`已排队至`}),(0,x.jsxs)(`dd`,{children:[`消息 #`,e.scan_cursor]})]}),(0,x.jsxs)(`div`,{children:[(0,x.jsx)(`dt`,{children:`已连续完成至`}),(0,x.jsxs)(`dd`,{children:[`消息 #`,e.applied_cursor]})]})]}),(0,x.jsx)(`p`,{className:`fine-print`,children:`页数和记忆条目数分别统计;未准入不等于失败。消息编号表示处理位置,不是发生时间;连续完成位置不会越过观察缺口。`})]}):null}function Or(e){return e.state===`applied`?e.kind===`admit`?`本页通过`:`已完成`:e.state===`rejected`?e.kind===`admit`?`本页未通过`:`未通过`:{queued:`排队中`,running:`处理中`,failed:`本次失败`,cancelled:`已取消`,deferred:`稍后重试`}[e.state]||e.state}function kr({work:e}){return e?.length?(0,x.jsxs)(`details`,{className:`history profile-work-list`,children:[(0,x.jsxs)(`summary`,{children:[`本次任务分块与检查 · `,e.length,` 项`]}),(0,x.jsx)(`p`,{className:`fine-print`,children:`每页单独检查;全部页通过后才可能准入,还需核对最终正文与依据。宿主是否能看到,以每条画像的投影状态为准。`}),e.map(e=>(0,x.jsxs)(`article`,{"aria-label":`工作项 #${e.id}`,className:`profile-work-card`,children:[(0,x.jsxs)(`div`,{className:`detail-badges`,children:[(0,x.jsx)(`strong`,{children:e.kind===`observe`?`观察消息分块`:`画像准入检查`}),(0,x.jsx)(M,{tone:e.state===`failed`||e.state===`rejected`?`danger`:``,children:Or(e)}),(0,x.jsxs)(`span`,{className:`muted`,children:[`工作项 #`,e.id]})]}),e.memory_id!=null&&(0,x.jsxs)(`button`,{className:`text-button`,onClick:()=>document.getElementById(`profile-memory-${e.memory_id}`)?.scrollIntoView?.({behavior:`smooth`,block:`start`}),children:[`画像记忆 #`,e.memory_id]}),e.kind===`admit`&&(0,x.jsx)(`p`,{children:e.check_page!=null&&e.check_pages!=null?`检查第 ${e.check_page} / ${e.check_pages} 页`:`检查页码未记录`}),e.kind===`admit`&&e.state===`applied`&&(0,x.jsx)(`p`,{className:`fine-print`,children:`全部页通过后才可能准入,本页通过不代表整条已展示。`}),(0,x.jsxs)(`dl`,{className:`profile-work-metadata`,children:[(0,x.jsxs)(`div`,{children:[(0,x.jsx)(`dt`,{children:`所属日期(角色时区)`}),(0,x.jsx)(`dd`,{children:e.local_day||`未记录`})]}),e.kind===`observe`&&(0,x.jsxs)(`div`,{children:[(0,x.jsx)(`dt`,{children:`消息编号范围`}),(0,x.jsxs)(`dd`,{children:[`#`,e.cursor_before,` 之后至 #`,e.cursor_after]})]}),(0,x.jsxs)(`div`,{children:[(0,x.jsx)(`dt`,{children:`执行次数`}),(0,x.jsxs)(`dd`,{children:[e.attempts,` 次`]})]}),(0,x.jsxs)(`div`,{children:[(0,x.jsx)(`dt`,{children:`下次重试`}),(0,x.jsx)(`dd`,{children:e.retry_at?w(e.retry_at):`未安排`})]}),(0,x.jsxs)(`div`,{children:[(0,x.jsx)(`dt`,{children:`入队时间`}),(0,x.jsx)(`dd`,{children:w(e.created_at)})]}),e.finished_at&&(0,x.jsxs)(`div`,{children:[(0,x.jsx)(`dt`,{children:`结束时间`}),(0,x.jsx)(`dd`,{children:w(e.finished_at)})]})]}),e.reason&&(0,x.jsx)(`p`,{children:_r(e.reason)})]},e.id))]}):null}function Ar({base:e,initial:t,settled:n,progress:r}){let[i,a]=(0,l.useState)(t.state===`running`),o=y(`${e}/attempts/${t.id}`,i?1500:0),s=(0,l.useRef)(!1);(0,l.useEffect)(()=>{o.data&&o.data.state!==`running`&&!s.current&&(s.current=!0,a(!1),n(o.data))},[o.data,n]);let c=i&&o.data||t;return(0,x.jsxs)(`section`,{className:`entry-profile-task`,"aria-label":`画像更新任务`,children:[(0,x.jsxs)(`h3`,{children:[`更新任务 #`,c.id]}),o.error&&(0,x.jsxs)(A,{error:!0,children:[o.error,(0,x.jsx)(`button`,{onClick:o.refresh,children:`重试任务状态`})]}),(0,x.jsx)(`p`,{role:`status`,children:c.state===`running`?c.stage===`checking`?`正在检查画像条目…`:`正在观察消息分块…`:c.state===`published`?`本轮已提交处理结果,仍以逐条准入状态为准。`:fr(c.state)}),c.reason&&(0,x.jsx)(`p`,{children:_r(c.reason)}),(0,x.jsxs)(`p`,{className:`fine-print`,children:[`开始:`,w(c.started_at),c.finished_at&&` · 结束:${w(c.finished_at)}`]}),(0,x.jsx)(Dr,{progress:c.progress||r}),(0,x.jsx)(kr,{work:c.work}),c.version!=null&&(0,x.jsxs)(`p`,{children:[`任务完成时的宿主投影版本:v`,c.version]})]})}function jr({entry:e,onClose:t,openMemory:n}){let r=`/entries/${encodeURIComponent(e.id)}/profile`,i=y(r,5e3),a=y(`/catalog`),o=Object.fromEntries((a.data?.people||[]).map(e=>[e.id,e.name])),[s,c]=(0,l.useState)(!1),[u,d]=(0,l.useState)(``),[p,m]=(0,l.useState)(``),[h,b]=(0,l.useState)(!1),[S,C]=(0,l.useState)(null),[T,E]=(0,l.useState)(null),D=(0,l.useRef)(!1),O=(0,l.useRef)(!0);(0,l.useEffect)(()=>(O.current=!0,()=>{O.current=!1}),[]);let k=async(e,t,n)=>{if(D.current)throw new f(`busy`,`正在保存,请稍候。`,409);D.current=!0,c(!0),d(``),m(``);try{return await g(e,_(t,n))}finally{D.current=!1,O.current&&(c(!1),i.refresh())}},j=(0,l.useCallback)(e=>{E(e),i.refresh()},[i.refresh]),N=i.data,P=N?.latest_attempt&&(!T||N.latest_attempt.id>T.id||N.latest_attempt.id===T.id&&(T.state===`running`||N.latest_attempt.state!==`running`))?N.latest_attempt:T||N?.latest_attempt,F=s||i.loading||!!i.error,I=P?.state===`running`,te=e=>{s||(t(),n(e))};async function L(){if(S&&!F)try{await k(`${r}/settings`,`PATCH`,S),C(null),m(`画像设置已保存。`)}catch(e){d(e instanceof f&&e.status===409?`画像已变化,设置草稿已保留;请核对最新投影后重新确认。`:v(e))}}async function ne(){if(!(!N||F||I))try{let e=await k(`${r}/regenerate`,`POST`,{expected_version:N.settings.current_version});E(e.attempt),m(`更新请求已提交,可关闭面板后再查看任务结果。`)}catch(e){d(v(e))}}return(0,x.jsx)(ee,{title:`入口画像 · ${e.name}`,onClose:t,closeDisabled:s,children:(0,x.jsxs)(`div`,{className:`entry-profile`,children:[(0,x.jsx)(`p`,{className:`muted`,children:`每条画像都是本入口的记忆,自动更新。手写内容按人工记忆保护。`}),(i.error||u)&&(0,x.jsxs)(A,{error:!0,children:[u||i.error,(0,x.jsx)(`button`,{className:`text-button`,disabled:s,onClick:i.refresh,children:`刷新画像`})]}),p&&(0,x.jsx)(A,{children:p}),N?N.supported?N.mode===`memory`?(0,x.jsxs)(x.Fragment,{children:[(0,x.jsxs)(`section`,{className:`entry-profile-section`,"aria-label":`画像设置`,children:[(0,x.jsxs)(`div`,{className:`button-row`,children:[(0,x.jsx)(M,{children:N.settings.enabled?`已开启`:`已关闭`}),(0,x.jsx)(`span`,{children:`自动更新,无需确认候选`})]}),!N.global_enabled&&(0,x.jsx)(A,{children:`全局画像开关已关闭,当前不会生成或加入回复准备。`}),S?(0,x.jsxs)(`form`,{onSubmit:e=>{e.preventDefault(),L()},children:[(0,x.jsxs)(`label`,{className:`check-label`,children:[(0,x.jsx)(`input`,{type:`checkbox`,checked:S.enabled,disabled:s,onChange:e=>C({...S,enabled:e.target.checked})}),`启用本入口画像`]}),S.expected_version!==N.settings.current_version&&(0,x.jsxs)(A,{children:[`投影已更新为 v`,N.settings.current_version,`,请先核对下方预览。`,(0,x.jsx)(`button`,{type:`button`,disabled:F,onClick:()=>C({...S,expected_version:N.settings.current_version}),children:`已核对,保留设置草稿`})]}),(0,x.jsxs)(`div`,{className:`button-row`,children:[(0,x.jsx)(`button`,{disabled:F||S.expected_version!==N.settings.current_version,children:`保存画像设置`}),(0,x.jsx)(`button`,{type:`button`,className:`secondary`,disabled:s,onClick:()=>C(null),children:`取消设置`})]})]}):(0,x.jsx)(`button`,{className:`secondary`,disabled:F,onClick:()=>C({enabled:N.settings.enabled,expected_version:N.settings.current_version}),children:`修改画像设置`})]}),(0,x.jsxs)(`section`,{className:`entry-profile-section`,"aria-label":`宿主投影预览`,children:[(0,x.jsxs)(`h3`,{children:[`宿主投影预览 · v`,N.settings.current_version]}),(0,x.jsx)(`p`,{className:`muted`,children:`以下为回复准备提供的原文;宿主传入已知记忆时会排除相应条目。投影版本独立于每条记忆的修订号。`}),N.projection?(0,x.jsxs)(x.Fragment,{children:[(0,x.jsx)(`p`,{className:`entry-profile-content profile-projection`,children:N.projection.text}),(0,x.jsxs)(`p`,{className:`fine-print`,children:[`version:`,N.projection.version,` · 更新于`,` `,w(N.projection.generated_at)]}),(0,x.jsx)(`div`,{className:`button-row`,children:N.projection.memory_ids.map(e=>(0,x.jsxs)(`button`,{className:`text-button`,onClick:()=>document.getElementById(`profile-memory-${e}`)?.scrollIntoView?.({behavior:`smooth`,block:`start`}),children:[`画像记忆 #`,e]},e))}),N.projection.possibly_stale&&(0,x.jsx)(A,{children:`投影可能过时:还有未处理消息、观察缺口、待检查或待重建的内容。`})]}):(0,x.jsx)(`p`,{children:`当前不向宿主提供画像(entry_profile 为 null)。`}),!!N.diagnostics.omitted_memory_ids?.length&&(0,x.jsxs)(A,{children:[`受展示预算限制,以下记忆未加入投影:`,N.diagnostics.omitted_memory_ids.map(e=>(0,x.jsxs)(`button`,{className:`text-button`,onClick:()=>document.getElementById(`profile-memory-${e}`)?.scrollIntoView?.({behavior:`smooth`,block:`start`}),children:[`#`,e]},e)),(0,x.jsx)(`p`,{children:`这些记忆仍然保留;人工内容不会因展示预算被隐藏。`})]}),N.rebuild_pending&&(0,x.jsx)(A,{children:`画像正在等待新任务重建或继续处理消息。`})]}),(0,x.jsxs)(`section`,{className:`entry-profile-section`,"aria-label":`每日增量进度`,children:[(0,x.jsx)(`h3`,{children:`每日增量进度`}),(0,x.jsx)(`p`,{className:`muted`,children:`自动任务按角色时区处理已结束日期;立即运行也处理当日尾块。准入由系统自动检查,无需审批。`}),(0,x.jsx)(`button`,{disabled:F||I||!N.settings.enabled||!N.global_enabled,onClick:()=>void ne(),children:`立即运行`}),(0,x.jsx)(`p`,{className:`fine-print`,children:`继续处理新消息和排队的检查;没有新依据时不会重复确认。`}),!P&&(0,x.jsx)(Dr,{progress:N.progress}),P&&(0,x.jsx)(Ar,{base:r,initial:P,settled:j,progress:N.progress},P.id)]}),(0,x.jsxs)(`section`,{className:`entry-profile-section`,"aria-label":`画像记忆`,children:[(0,x.jsxs)(`div`,{className:`panel-heading`,children:[(0,x.jsxs)(`h3`,{children:[`画像记忆 · `,N.memories.length,` 条`]}),(0,x.jsx)(`button`,{disabled:F||h,onClick:()=>b(!0),children:`人工新建`})]}),h&&(0,x.jsx)(wr,{entry:e,blocked:F,busy:s,write:k,cancel:()=>b(!1),created:()=>{b(!1),m(`已创建本入口的画像记忆。`)}}),!N.memories.length&&(0,x.jsx)(`p`,{children:`暂无画像记忆。可手写补充,也可等待新消息形成画像。`}),(0,x.jsx)(`div`,{className:`profile-memory-list`,children:N.memories.map(e=>(0,x.jsx)(Er,{memory:e,speakerNames:o,displayed:!!N.projection?.memory_ids.includes(e.memory_id),blocked:F,busy:s,write:k,refresh:i.refresh,openMemory:te},e.memory_id))})]})]}):(0,x.jsx)(A,{children:`入口画像尚未切换为记忆形式,请更新服务后刷新。旧版历史仍可只读查看。`}):(0,x.jsx)(A,{children:`私聊入口不生成入口画像。`}):(0,x.jsx)(`p`,{role:`status`,children:`正在读取入口画像…`}),N?.supported&&(0,x.jsxs)(x.Fragment,{children:[!!N.migration?.length&&(0,x.jsxs)(`details`,{className:`history`,children:[(0,x.jsx)(`summary`,{children:`旧画像迁移记录`}),(0,x.jsx)(`ul`,{children:N.migration.map(e=>(0,x.jsxs)(`li`,{children:[`旧版 v`,e.legacy_version,` · 原条目序号`,` `,e.sentence_index,`:`,{manual_imported:`已转为人工画像记忆`,manual_unclassified:`已转为管理员说明`,model_rebuild_pending:`原模型条目待按依据重建`,cleared_history:`原内容已清空`}[e.disposition]||e.disposition,e.memory_id!=null&&(0,x.jsxs)(`button`,{className:`text-button`,disabled:s,onClick:()=>te(e.memory_id),children:[`查看记忆 #`,e.memory_id]})]},`${e.legacy_version}:${e.sentence_index}`))})]}),(0,x.jsx)(yr,{base:r,openMemory:te})]})]})})}var Mr={forget:`遗忘`,delete:`删除`,purge:`彻底清除`},Nr={memory_source:`仍是其他记忆的来源`,batch_segment:`等待或运行中的批次仍在使用`,goal_source:`仍是目标的来源`,subject_alias:`仍是主体别名的证据`,subject_link:`仍是主体联系的证据`,learning_request:`立即学习请求仍在使用`};function Pr({scope:e,name:t,onClose:n,onChanged:r}){let[i,a]=(0,l.useState)(`forget`),[o,s]=(0,l.useState)(!1),[c,u]=(0,l.useState)(null),[d,p]=(0,l.useState)(null),[m,h]=(0,l.useState)(!1),[y,b]=(0,l.useState)(``),[S,C]=(0,l.useState)(!1),[w,T]=(0,l.useState)(!1),D=d?.status===`completed`,O=()=>{u(null),b(``),C(!1),T(!1)};async function k(){h(!0),O();try{u(await g(`/memories/bulk/preview`,_(`POST`,{...e,action:i,include_pinned:o})))}catch(e){b(v(e))}finally{h(!1)}}async function j(){if(c&&!m&&c.applicable_count&&(i!==`purge`||w)){h(!0),b(``);try{p(await g(`/memories/bulk/apply`,_(`POST`,{snapshot_token:c.snapshot_token,action:i,confirm:i===`purge`&&w}))),u(null),C(!1),r()}catch(e){e instanceof f&&e.status===409?(O(),e.result&&(p(e.result),r()),b(e.message)):b(`${v(e)}。执行结果尚未确认,可使用当前预览重试;若服务已重启,请先查看操作记录。`)}finally{h(!1)}}}return(0,x.jsx)(ee,{title:`批量遗忘/删除`,onClose:n,closeDisabled:m,children:(0,x.jsxs)(`div`,{className:`lifecycle-stack bulk-memory`,children:[(0,x.jsxs)(`p`,{children:[(0,x.jsx)(`strong`,{children:t}),` ·`,` `,`subject_id`in e?`人物 ${e.subject_id}`:`入口 ${e.entry_id}`]}),(0,x.jsx)(`p`,{className:`muted`,children:`subject_id`in e?`范围:说话人或涉及人物为此人的记忆。`:`范围:至少一条直接来源消息属于此入口的记忆。`}),y&&(0,x.jsx)(A,{error:!0,children:y}),d&&(0,x.jsxs)(`section`,{"aria-label":`批量操作结果`,children:[(0,x.jsx)(A,{error:!D,children:D?`已批量${Mr[i]} ${d.count} 条记忆。`:`操作未全部完成,已处理 ${d.count} 条记忆。`}),(0,x.jsx)($t,{notice:d.backup_notice}),d.skipped_count>0&&(0,x.jsxs)(`p`,{children:[`跳过已删除的 `,d.skipped_count,` 条记忆。`]}),(0,x.jsxs)(`details`,{children:[(0,x.jsxs)(`summary`,{children:[`已处理记忆 ID(`,d.memory_ids.length,` 条)`]}),(0,x.jsx)(`p`,{className:`bulk-ids`,children:d.memory_ids.map(e=>`#${e}`).join(`、`)||`无`})]}),i===`purge`&&(0,x.jsxs)(x.Fragment,{children:[(0,x.jsxs)(`p`,{children:[`已删除来源消息 `,d.deleted_message_ids.length,` 条;保留`,` `,d.retained_messages.length,` 条。`]}),(0,x.jsxs)(`details`,{children:[(0,x.jsx)(`summary`,{children:`来源消息处理明细`}),(0,x.jsxs)(`p`,{className:`bulk-ids`,children:[`已删除:`,d.deleted_message_ids.map(e=>`#${e}`).join(`、`)||`无`]}),d.retained_messages.map(e=>(0,x.jsxs)(`p`,{children:[`消息 #`,e.message_id,`:`,e.reasons.map(e=>Nr[e]||e).join(`;`)]},e.message_id))]})]})]}),!D&&(0,x.jsxs)(x.Fragment,{children:[!S&&(0,x.jsxs)(`fieldset`,{disabled:m,className:`bulk-options`,children:[(0,x.jsxs)(`label`,{children:[`目标操作`,(0,x.jsxs)(`select`,{value:i,onChange:e=>{a(e.target.value),O()},children:[(0,x.jsx)(`option`,{value:`forget`,children:`遗忘(可恢复)`}),(0,x.jsx)(`option`,{value:`delete`,children:`删除(撤销对象)`}),(0,x.jsx)(`option`,{value:`purge`,children:`彻底清除(不可撤销)`})]})]}),(0,x.jsxs)(`label`,{className:`check-label`,children:[(0,x.jsx)(`input`,{type:`checkbox`,checked:o,onChange:e=>{s(e.target.checked),O()}}),`包含置顶记忆`]}),(0,x.jsx)(`p`,{children:i===`forget`?`遗忘会降低保留强度并取消置顶;仍可深度召回及恢复。`:i===`delete`?`删除会撤销记忆对象,保留来源与历史;普通和深度召回均不再返回。`:`彻底清除会清除正文、修订历史,以及没有其他受保护引用的来源消息。`}),(0,x.jsx)(`button`,{className:`secondary`,onClick:()=>void k(),children:m?`正在处理…`:`预览范围`})]}),c&&(0,x.jsxs)(`section`,{"aria-label":`批量操作预览`,children:[(0,x.jsxs)(`h3`,{children:[`范围内 `,c.total,` 条,本次处理 `,c.applicable_count,` `,`条`]}),(0,x.jsxs)(`p`,{children:[`有效 `,c.counts.active,` · 遗忘 `,c.counts.forgotten,` `,`· 已删除 `,c.counts.deleted]}),(0,x.jsxs)(`p`,{children:[`包含置顶 `,c.pinned_count,` 条;排除置顶`,` `,c.excluded_pinned_count,` 条。`]}),(0,x.jsx)(`p`,{className:`muted`,children:`已删除对象只参加彻底清除。预览有效期 15 分钟,记忆变化后需重新预览。`}),(0,x.jsx)(`ul`,{className:`bulk-examples`,children:c.examples.map(e=>(0,x.jsxs)(`li`,{children:[(0,x.jsxs)(`span`,{className:`muted`,children:[`#`,e.id,` · `,E(e.lifecycle),e.pinned?` · 置顶`:``]}),(0,x.jsx)(`p`,{children:e.content})]},e.id))}),i===`purge`&&(0,x.jsx)($t,{notice:c.backup_notice}),S?(0,x.jsxs)(x.Fragment,{children:[(0,x.jsxs)(A,{error:!0,children:[`即将彻底清除 `,c.applicable_count,` `,`条记忆,此操作不可撤销。同时清空可关联的模型原始输出等正文副本。`]}),(0,x.jsxs)(`label`,{className:`check-label`,children:[(0,x.jsx)(`input`,{type:`checkbox`,disabled:m,checked:w,onChange:e=>T(e.target.checked)}),`我理解此操作不可撤销`]}),(0,x.jsxs)(`div`,{className:`button-row`,children:[(0,x.jsx)(`button`,{className:`danger-button`,disabled:m||!w,onClick:()=>void j(),children:`再次确认并彻底清除`}),(0,x.jsx)(`button`,{className:`secondary`,disabled:m,onClick:()=>{C(!1),T(!1)},children:`返回预览`})]})]}):(0,x.jsxs)(`button`,{className:`danger-button`,disabled:m||!c.applicable_count,onClick:()=>i===`purge`?C(!0):void j(),children:[`确认批量`,Mr[i]]})]})]}),m&&(0,x.jsx)(`p`,{role:`status`,children:`正在处理,请等待结果…`}),!m&&(0,x.jsx)(`a`,{href:`#/operations?object_type=memory_bulk`,onClick:n,children:`查看批量操作记录`}),D&&(0,x.jsx)(`button`,{className:`primary`,onClick:n,children:`完成`})]})})}var Fr=e=>({group:`群聊`,private:`私聊`,live:`直播`})[e]||e,Ir=e=>({message:`消息`,self_output:`角色实际输出`,action_result:`行动结果`,event:`场景事件`})[e]||e,Lr=e=>({attempts_exhausted:`尝试用尽,未学习`,content_rejection:`内容拒绝,未学习`,interrupted:`学习中断`})[e]||e;function Rr(e){let t=`已自动重试 ${e.attempted}/${e.total}`;return{scheduled:`${t},下次 ${e.next_date}`,in_progress:e.attempted?t:``,finished:`${t},不再自动重试,可手动重新学习`,disabled:`${t},自动重试已关闭`,targets_cleared:`${t},目标消息已清理,不再自动重试`,refused:`内容拒绝,不自动重试,可手动重新学习`}[e.status]}var zr=e=>typeof e==`string`?e:JSON.stringify(e,null,2);function Br({openMemory:e}){let[t,n]=(0,l.useState)(`entries`),[r,i]=(0,l.useState)(null),[a,o]=(0,l.useState)(null),[s,c]=(0,l.useState)(null),[u,d]=(0,l.useState)(null),[f,p]=(0,l.useState)(``),[m,h]=(0,l.useState)({entry_id:``,time_from:``,time_to:``}),[g,_]=(0,l.useState)(m),[v,b]=(0,l.useState)(0),C=y(`/entries`,5e3),w=new URLSearchParams({limit:`30`,offset:String(v)});for(let[e,t]of Object.entries(g))t&&w.set(e,t);let T=y(t===`batches`&&r===null?`/batches?${w}`:null,3e3),E=y(t===`gaps`&&r===null?`/memory-gaps?${w}`:null,3e3),D=t===`batches`?T:E,O=()=>{C.refresh(),T.refresh(),E.refresh()};function k(e){n(e),i(null),b(0)}function ee(e){let t={entry_id:e,time_from:``,time_to:``};h(t),_(t),k(`batches`)}return(0,x.jsxs)(x.Fragment,{children:[(0,x.jsx)(`div`,{className:`page-heading`,children:(0,x.jsxs)(`div`,{children:[(0,x.jsx)(`p`,{className:`eyebrow`,children:`从消息到记忆`}),(0,x.jsx)(`h1`,{children:`入口与学习`}),(0,x.jsx)(`p`,{children:`查看每个入口的学习进度,追溯批次结果与尚未记住的消息。`})]})}),(0,x.jsx)(`div`,{className:`learning-tabs`,role:`group`,"aria-label":`入口与学习视图`,children:[[`entries`,`入口列表`],[`batches`,`学习批次`],[`gaps`,`记忆缺口`]].map(([e,n])=>(0,x.jsx)(`button`,{"aria-pressed":t===e,onClick:()=>k(e),children:n},e))}),C.error&&(0,x.jsxs)(A,{error:!0,children:[C.error,(0,x.jsx)(`button`,{onClick:C.refresh,children:`重试`})]}),f&&(0,x.jsx)(A,{children:f}),a&&(0,x.jsx)(Pr,{scope:{entry_id:a.id},name:a.name,onClose:()=>o(null),onChanged:O}),u&&(0,x.jsx)(jr,{entry:u,onClose:()=>d(null),openMemory:e},u.id),s&&(0,x.jsx)(ct,{entry:s,entries:C.data?.items||[],onClose:()=>c(null),onSaved:()=>{c(null),p(`入口设置已保存`),O()}}),r===null?t===`entries`?(0,x.jsxs)(x.Fragment,{children:[(0,x.jsx)(`p`,{className:`learning-intro`,children:`已接收的消息可能仍在等待学习;批次结束后,也可能没有形成记忆。`}),C.data?C.data.items.length?(0,x.jsx)(`div`,{className:`entry-grid`,children:C.data.items.map(e=>(0,x.jsxs)(`article`,{className:`panel learning-entry`,children:[(0,x.jsxs)(`div`,{className:`section-title`,children:[(0,x.jsx)(`h2`,{children:e.name}),(0,x.jsx)(M,{children:Fr(e.kind)})]}),(0,x.jsx)(`p`,{className:`muted`,children:e.platform===`iris-trial`?`试用入口`:`宿主入口 · ${e.platform}`}),(0,x.jsxs)(`div`,{className:`learning-entry-meta`,children:[(0,x.jsx)(M,{children:at(e.pace)}),(0,x.jsxs)(`strong`,{children:[`待学习 `,e.pending_count,` 条`]})]}),(0,x.jsxs)(`p`,{className:`entry-filter-summary`,children:[`记忆可见范围:`,tt(e,C.data?.items||[])]}),(0,x.jsx)(ot,{value:e.filters}),(0,x.jsx)(st,{value:e.queue_wait}),(0,x.jsxs)(`p`,{children:[`最近批次`,e.latest_batch?` #${e.latest_batch.id}`:``]}),(0,x.jsx)(`p`,{children:e.latest_batch?Qn(e.latest_batch):`暂无批次`}),e.current_batch&&e.current_batch.id!==e.latest_batch?.id&&(0,x.jsxs)(`p`,{children:[`当前处理 #`,e.current_batch.id,` ·`,` `,Qn(e.current_batch)]}),(0,x.jsx)(`button`,{className:`secondary`,"aria-label":`修改入口设置 · ${e.name}`,onClick:()=>{p(``),c(e)},children:`修改入口设置`}),(0,x.jsx)(`button`,{className:`secondary`,"aria-label":`入口画像 · ${e.name}`,onClick:()=>d(e),children:`入口画像`}),(0,x.jsx)(`button`,{className:`secondary`,"aria-label":`批量遗忘/删除 · ${e.name}`,onClick:()=>o(e),children:`批量遗忘/删除`}),(0,x.jsx)(`button`,{className:`text-button`,"aria-label":`查看批次 · ${e.name}`,onClick:()=>ee(e.id),children:`查看批次 →`})]},e.id))}):(0,x.jsx)(j,{title:`还没有入口`,children:`可在试用对话页创建入口,或由宿主发送消息。`}):(0,x.jsx)(`p`,{children:`正在读取入口…`})]}):(0,x.jsxs)(x.Fragment,{children:[(0,x.jsx)(`p`,{className:`learning-intro`,children:t===`gaps`?`放弃或内容拒绝的批次留下记忆缺口。重新学习成功后才会消失;模型暂停期间的普通积压不算缺口。`:`批次保留冻结的三段消息。成功可以形成零条记忆;失败重试和放弃、拒绝会分别显示。`}),(0,x.jsxs)(`form`,{className:`learning-filters`,onSubmit:e=>{e.preventDefault(),_({...m}),b(0)},children:[(0,x.jsxs)(`label`,{children:[`筛选入口`,(0,x.jsxs)(`select`,{value:m.entry_id,onChange:e=>h({...m,entry_id:e.target.value}),children:[(0,x.jsx)(`option`,{value:``,children:`全部入口`}),C.data?.items.map(e=>(0,x.jsx)(`option`,{value:e.id,children:e.name},e.id))]})]}),(0,x.jsxs)(`label`,{children:[`开始日期`,(0,x.jsx)(`input`,{type:`date`,value:m.time_from,onChange:e=>h({...m,time_from:e.target.value})})]}),(0,x.jsxs)(`label`,{children:[`结束日期`,(0,x.jsx)(`input`,{type:`date`,value:m.time_to,min:m.time_from||void 0,onChange:e=>h({...m,time_to:e.target.value})})]}),(0,x.jsx)(`button`,{className:`secondary`,type:`submit`,children:`筛选`})]}),(0,x.jsxs)(`p`,{className:`muted learning-date-help`,children:[`日期按角色时区;`,t===`gaps`?`显示与日期范围重叠的消息区间。`:`按批次组成时间筛选。`]}),D.error&&(0,x.jsxs)(A,{error:!0,children:[D.error,(0,x.jsx)(`button`,{onClick:D.refresh,children:`重试`})]}),t===`batches`&&T.data?.entry_waits?.filter(e=>e.queue_wait.reason).map(e=>(0,x.jsxs)(`div`,{className:`panel`,children:[(0,x.jsxs)(`strong`,{children:[C.data?.items.find(t=>t.id===e.entry_id)?.name||e.entry_id,` `,`· 当前入口等待`]}),(0,x.jsx)(st,{value:e.queue_wait})]},e.entry_id)),D.data?(0,x.jsxs)(x.Fragment,{children:[D.data.total===0?(0,x.jsx)(j,{title:t===`gaps`?`当前范围没有记忆缺口`:`当前范围没有批次`}):(0,x.jsx)(`div`,{className:`learning-list`,children:t===`batches`?T.data?.items.map(e=>(0,x.jsxs)(`article`,{className:`panel learning-batch-card`,children:[(0,x.jsxs)(`div`,{className:`learning-row`,children:[(0,x.jsxs)(`div`,{children:[(0,x.jsxs)(`strong`,{children:[`#`,e.id,` · `,e.entry_name]}),(0,x.jsx)(`p`,{children:Qn(e)}),(0,x.jsxs)(`small`,{children:[S(e.created_at),` ·`,` `,Xn(e)?`拆分前`:`本轮`,`已计`,` `,e.attempt_count,` 次尝试`]})]}),(0,x.jsx)(`button`,{className:`text-button`,"aria-label":`查看批次 #${e.id}`,onClick:()=>i(e.id),children:`查看详情 →`})]}),(0,x.jsx)(tr,{batch:e,onOpen:i}),Xn(e)&&(0,x.jsxs)(`div`,{className:`learning-batch-tree`,children:[(0,x.jsx)(er,{batch:e}),(0,x.jsx)(sr,{id:e.id,onOpen:i})]})]},e.id)):E.data?.items.map(e=>(0,x.jsxs)(`article`,{className:`panel learning-row`,children:[(0,x.jsxs)(`div`,{children:[(0,x.jsxs)(`strong`,{children:[e.entry_name,` · 批次 #`,e.batch_id]}),(0,x.jsxs)(`p`,{children:[S(e.started_at),` — `,S(e.ended_at)]}),(0,x.jsx)(`p`,{children:Lr(e.reason)}),Rr(e.auto_retry)&&(0,x.jsx)(`p`,{children:Rr(e.auto_retry)}),e.relearning&&(0,x.jsx)(M,{children:`重新学习中`})]}),(0,x.jsx)(`button`,{className:`text-button`,"aria-label":`查看批次 #${e.batch_id}`,onClick:()=>i(e.batch_id),children:`查看详情 →`})]},e.batch_id))}),(0,x.jsxs)(`div`,{className:`learning-pagination`,children:[(0,x.jsxs)(`span`,{children:[`共 `,D.data.total,` `,t===`batches`?`个批次节点(含子批次)`:`处缺口`]}),(0,x.jsx)(`button`,{disabled:v===0,onClick:()=>b(Math.max(0,v-30)),children:`上一页`}),(0,x.jsx)(`button`,{disabled:v+30>=D.data.total,onClick:()=>b(v+30),children:`下一页`})]})]}):(0,x.jsx)(`p`,{children:`正在读取…`})]}):(0,x.jsx)(Ur,{id:r,openMemory:e,onBack:()=>i(null),onRelearn:O,onOpen:i},r)]})}function Vr({calls:e}){return e.length?(0,x.jsx)(`div`,{className:`learning-calls`,children:e.map(e=>(0,x.jsxs)(`div`,{children:[(0,x.jsxs)(`strong`,{children:[e.purpose===`learning_repair`?`JSON 修正请求`:`学习请求`,` `,`· `,e.model]}),(0,x.jsxs)(`dl`,{children:[(0,x.jsxs)(`div`,{children:[(0,x.jsx)(`dt`,{children:`请求结果`}),(0,x.jsxs)(`dd`,{children:[e.result_category===`success`?`已返回正文`:e.result_category,e.timed_out?` · 超时`:``]})]}),(0,x.jsxs)(`div`,{children:[(0,x.jsx)(`dt`,{children:`耗时`}),(0,x.jsx)(`dd`,{children:C(e.duration_ms)})]}),(0,x.jsxs)(`div`,{children:[(0,x.jsx)(`dt`,{children:`结束原因(finish_reason)`}),(0,x.jsx)(`dd`,{children:e.finish_reason||`未提供`})]}),(0,x.jsxs)(`div`,{children:[(0,x.jsx)(`dt`,{children:`推理档位`}),(0,x.jsx)(`dd`,{children:e.reasoning_effort||`未记录 / 服务商默认`})]})]}),e.error_summary&&(0,x.jsx)(`p`,{children:e.error_summary})]},e.id))}):(0,x.jsx)(`p`,{className:`muted`,children:`没有可关联的调用记录;结束原因与推理档位未知。`})}function Hr({label:e,value:t}){return t==null?(0,x.jsxs)(`p`,{className:`muted`,children:[e,`:没有记录`]}):(0,x.jsxs)(`details`,{className:`learning-output`,children:[(0,x.jsx)(`summary`,{children:e}),(0,x.jsx)(`pre`,{children:t||`(空正文)`})]})}function Ur({id:e,onBack:t,openMemory:n,onRelearn:r,onOpen:i}){let a=y(`/batches/${e}`,3e3),[o,s]=(0,l.useState)(!1),[c,u]=(0,l.useState)(``),[d,f]=(0,l.useState)(``),p=a.data;async function m(){if(!o&&p?.can_relearn&&!Xn(p)&&[`abandoned`,`refused`].includes(p.state)){s(!0),f(``),u(``);try{await g(`/batches/${e}/relearn`,_(`POST`,{})),u(`已重新排队,尚未记住。模型恢复且本入口在途批次结束后开始学习。`),r()}catch(e){f(v(e))}finally{s(!1),a.refresh()}}}return(0,x.jsxs)(`section`,{"aria-label":`批次 #${e}`,className:`batch-detail`,children:[(0,x.jsx)(`button`,{className:`text-button`,onClick:t,children:`← 返回列表`}),(a.error||d)&&(0,x.jsx)(A,{error:!0,children:d||a.error}),c&&(0,x.jsx)(A,{children:c}),p?(0,x.jsxs)(x.Fragment,{children:[(0,x.jsxs)(`div`,{className:`panel batch-overview`,children:[(0,x.jsxs)(`div`,{className:`section-title`,children:[(0,x.jsxs)(`h2`,{children:[`批次 #`,e,` · `,p.entry.name]}),(0,x.jsx)(M,{children:Qn(p,p.segments.target.length)})]}),(0,x.jsx)(tr,{batch:p,onOpen:i}),(0,x.jsxs)(`p`,{className:`muted`,children:[`组成于 `,S(p.created_at),p.finished_at?` · 结束于 ${S(p.finished_at)}`:``,` `,`· `,Xn(p)?`拆分前`:`本轮`,`已计`,` `,p.attempt_count,` / 4 次尝试`]}),p.parent_batch_id&&(0,x.jsx)(rr,{id:e}),(0,x.jsx)(ir,{batch:p}),Xn(p)&&(0,x.jsx)(er,{batch:p}),!Xn(p)&&p.gap&&(0,x.jsx)(`p`,{children:`仍有记忆缺口;成功学习后才会消失。`}),p.last_error&&(0,x.jsxs)(`p`,{children:[Xn(p)?`子批次最近错误`:`最近错误`,`:`,p.last_error]}),p.relearn_blocked_reason&&!Xn(p)&&(0,x.jsx)(`p`,{children:p.relearn_blocked_reason}),p.can_relearn&&!Xn(p)&&[`abandoned`,`refused`].includes(p.state)&&(0,x.jsxs)(`div`,{className:`relearn-action`,children:[(0,x.jsx)(`button`,{className:`primary`,disabled:o,onClick:m,children:o?`正在提交…`:`重新学习`}),(0,x.jsx)(`p`,{children:`保留原批次范围和历史记录,重新获得 4 次尝试;成功前缺口持续存在。`})]}),p.state===`succeeded`&&!p.memories.length&&(0,x.jsx)(`p`,{children:`本批已学习,形成或更新了 0 条记忆。这是正常的成功结果。`}),!Xn(p)&&p.state!==`succeeded`&&(0,x.jsx)(`p`,{children:`尚未成功学习,不能把这些消息当作已经记住。`})]}),Xn(p)&&(0,x.jsxs)(`section`,{className:`panel`,children:[(0,x.jsx)(`h2`,{children:`子批次结果`}),(0,x.jsx)(`p`,{children:`本节点的历史请求曾被拒绝,已拆分交给子批次。请进入失败子批次详情重新学习。`}),(0,x.jsx)(sr,{id:e,onOpen:i})]}),(0,x.jsxs)(`section`,{children:[(0,x.jsx)(`h2`,{children:Xn(p)?`拆分前冻结的三段消息`:`三段消息`}),(0,x.jsxs)(`details`,{className:`panel`,children:[(0,x.jsx)(`summary`,{children:`组成批次时的入口设置`}),p.entry_settings?(0,x.jsxs)(x.Fragment,{children:[(0,x.jsxs)(`p`,{children:[`学习节奏:`,at(p.entry_settings.pace)]}),(0,x.jsx)(ot,{value:p.entry_settings.filters})]}):(0,x.jsx)(`p`,{children:`该批次没有保存入口设置快照。`})]}),(0,x.jsx)(`p`,{className:`muted`,children:`三段范围已冻结;历史与后续只帮助理解,只有目标段可以形成记忆。这里保留消息原文。`}),p.context_mode===`target_only`&&(0,x.jsx)(`p`,{className:`notice`,children:`当前仅目标模式:下方历史段与后续段保留供追溯,不会作为可选前后文发送;仍保留目标引用、附件及既有相关记忆等材料。`}),(0,x.jsx)(`div`,{className:`batch-segments`,children:[[`history`,`历史段`,`只帮助理解`],[`target`,`目标段`,`只从这里学习`],[`future`,`后续段`,`以后再学习`]].map(([e,t,n])=>(0,x.jsxs)(`section`,{className:`panel segment segment-${e}`,children:[(0,x.jsxs)(`div`,{className:`section-title`,children:[(0,x.jsxs)(`h3`,{children:[t,` · `,p.segments[e].length,` 条`]}),(0,x.jsx)(`small`,{children:n})]}),!p.segments[e].length&&(0,x.jsx)(`p`,{className:`muted`,children:`本段为空`}),p.segments[e].map(e=>(0,x.jsx)(`article`,{className:`batch-message`,children:e.missing?(0,x.jsxs)(`p`,{children:[`消息 #`,e.id,` 已不可用`]}):(0,x.jsxs)(x.Fragment,{children:[(0,x.jsxs)(`div`,{children:[(0,x.jsx)(`strong`,{children:e.sender_name||e.sender_subject_id}),` `,(0,x.jsx)(M,{children:Ir(e.kind)})]}),(0,x.jsxs)(`small`,{children:[`#`,e.id,` · `,S(e.occurred_at),e.scene_identity?` · ${e.scene_identity}`:``]}),e.content.length>600?(0,x.jsxs)(`details`,{children:[(0,x.jsxs)(`summary`,{children:[e.content.slice(0,120),`…(展开原文)`]}),(0,x.jsx)(`p`,{className:`message-body`,children:e.content})]}):(0,x.jsx)(`p`,{className:`message-body`,children:e.content}),(0,x.jsx)(qe,{media:e.media}),e.quote_content&&(0,x.jsxs)(`blockquote`,{children:[e.quote_author_name||`引用`,`:`,e.quote_content]}),(0,x.jsx)(`small`,{children:O(e.learning_state)})]})},e.id))]},e))})]}),(0,x.jsxs)(`section`,{children:[(0,x.jsx)(`h2`,{children:Xn(p)?`本节点历史尝试(拆分前)`:`每次尝试`}),Xn(p)&&(0,x.jsx)(`p`,{children:`以下只记录本节点拆分前的请求;这些历史拒绝不等于子批次最终失败。`}),(0,x.jsxs)(`p`,{children:[`本节点实际模型调用 `,$n(p),` 次`]}),(0,x.jsx)(`p`,{className:`muted`,children:`含学习、JSON 修正和未归入尝试的调用;不包含子批次调用,不等于树节点数或尝试额度。`}),(0,x.jsx)(`p`,{className:`muted`,children:`历次尝试持续编号;服务暂停时的失败记录不一定扣除尝试额度。模型返回正文不代表已经写入记忆。`}),!p.attempts.length&&(0,x.jsx)(j,{title:p.state===`running`?`本次尝试仍在进行`:`尚无已完成的尝试`}),p.attempts.map(e=>(0,x.jsxs)(`article`,{className:`panel batch-attempt`,children:[(0,x.jsxs)(`div`,{className:`section-title`,children:[(0,x.jsxs)(`h3`,{children:[`第 `,e.number,` 次尝试`]}),(0,x.jsx)(M,{children:e.parse_status===`failed`?`失败 · 未写入`:`学习成功`})]}),(0,x.jsxs)(`p`,{children:[S(e.started_at),` — `,S(e.finished_at),` · 总耗时 `,C(e.duration_ms)]}),(0,x.jsxs)(`p`,{children:[`解析结果:`,{direct:`直接解析`,repaired:`修正后解析`,failed:`失败`}[e.parse_status]||e.parse_status]}),(0,x.jsxs)(`p`,{children:[`本次上下文模式:`,Zn(e.context_mode)]}),e.error&&(0,x.jsxs)(`p`,{children:[`诊断:`,e.error]}),(0,x.jsx)(Vr,{calls:e.calls}),(0,x.jsx)(Hr,{label:`模型原始正文`,value:e.raw_output}),(0,x.jsx)(Hr,{label:`JSON 修正后的正文`,value:e.repair_output})]},e.id)),!!p.unassigned_calls.length&&(0,x.jsxs)(`details`,{className:`panel batch-attempt`,children:[(0,x.jsxs)(`summary`,{children:[`尚未归入已完成尝试的调用 · `,p.unassigned_calls.length,` 条`]}),(0,x.jsx)(Vr,{calls:p.unassigned_calls})]})]}),(0,x.jsxs)(`div`,{className:`batch-results`,children:[(0,x.jsxs)(`section`,{className:`panel`,children:[(0,x.jsxs)(`h2`,{children:[`规整记录 · `,p.result.normalizations?.length||0,` 条`]}),!p.result.normalizations?.length&&(0,x.jsx)(`p`,{className:`muted`,children:`没有规整记录。`}),p.result.normalizations?.map((e,t)=>(0,x.jsxs)(`article`,{className:`result-item`,children:[(0,x.jsx)(`strong`,{children:e.field}),(0,x.jsx)(`p`,{children:e.reason}),(0,x.jsxs)(`small`,{children:[e.section,e.index==null?``:` · 第 ${e.index+1} 项`]}),(0,x.jsxs)(`pre`,{children:[zr(e.before),` → `,zr(e.after)]})]},t))]}),(0,x.jsxs)(`section`,{className:`panel`,children:[(0,x.jsxs)(`h2`,{children:[`被丢弃的条目 · `,p.result.dropped?.length||0,` 条`]}),!p.result.dropped?.length&&(0,x.jsx)(`p`,{className:`muted`,children:`没有已记录的丢弃条目;批次失败时请查看原始输出和错误。`}),p.result.dropped?.map((e,t)=>(0,x.jsxs)(`article`,{className:`result-item`,children:[(0,x.jsx)(`strong`,{children:e.reason}),(0,x.jsx)(`small`,{children:e.section}),(0,x.jsx)(`pre`,{children:zr(e.item)})]},t))]})]}),(0,x.jsxs)(`section`,{className:`panel`,children:[(0,x.jsxs)(`h2`,{children:[Xn(p)?`成功子批次的记忆结果`:`记忆结果`,` ·`,` `,p.memories.length,` 条`]}),(0,x.jsx)(`p`,{className:`muted`,children:`下列正文为记忆当前版本;详情中可查看修订历史。`}),!p.memories.length&&(0,x.jsx)(`p`,{children:p.state===`succeeded`?`本批没有新增、更新或确认记忆。`:`尚无成功写入的记忆。`}),p.memories.map(e=>(0,x.jsxs)(`div`,{className:`learning-row`,children:[(0,x.jsx)(M,{children:{created:`新增`,updated:`更新`,confirmed:`再次确认`}[e.change]||e.change}),e.missing?(0,x.jsxs)(`span`,{children:[`记忆 #`,e.id,` 已不可用`]}):(0,x.jsxs)(`button`,{className:`text-button`,onClick:()=>n(e.id),children:[`#`,e.id,` · `,e.content]}),(0,x.jsxs)(`small`,{children:[E(e.lifecycle),` · 修订 `,e.revision]})]},`${e.change}-${e.id}`))]})]}):(0,x.jsx)(`p`,{children:`正在读取批次…`})]})}function Wr(e){if(!e||typeof e!=`object`)return null;let t=e;return typeof t.reason!=`string`||!Array.isArray(t.items)||!t.items.every(e=>{if(!e||typeof e!=`object`)return!1;let t=e;return Number.isInteger(t.index)&&typeof t.supported==`boolean`&&typeof t.scene_qualified==`boolean`&&typeof t.reason==`string`&&Array.isArray(t.violations)&&t.violations.every(e=>typeof e==`string`)})?null:t}function Gr(e){if(!e||typeof e!=`object`)return`原条目格式无效或未保存`;let t=e.text;return typeof t==`string`?t:`原条目格式无效或未保存`}function Kr({value:e,openMemory:t}){return e.length?(0,x.jsxs)(A,{children:[(0,x.jsx)(`strong`,{children:`可能过时`}),(0,x.jsx)(`ul`,{children:e.map((e,n)=>(0,x.jsxs)(`li`,{children:[e.memory_id!=null&&(0,x.jsxs)(`button`,{className:`text-button`,onClick:()=>t(e.memory_id),children:[`记忆 #`,e.memory_id]}),e.revision!=null&&`(依据修订 ${e.revision})`,e.sentence_index!=null&&`第 ${e.sentence_index+1} 条:`,Ft(e.reason||``)]},n))})]}):null}function qr({item:e,openMemory:t}){return(0,x.jsxs)(`div`,{className:`summary-item`,children:[(0,x.jsxs)(`p`,{children:[e.kind&&(0,x.jsx)(M,{children:Nt[e.kind]||e.kind}),` `,(0,x.jsx)(M,{children:e.admin_written||e.author===`admin`?`管理员手写`:`模型生成`})]}),(0,x.jsx)(`p`,{className:`entry-profile-content`,children:e.body||e.text}),e.time_range&&(0,x.jsxs)(`p`,{children:[`时间范围:`,e.time_range]}),e.valid_until&&(0,x.jsxs)(`p`,{children:[`有效至:`,e.valid_until]}),e.expires_at&&(0,x.jsxs)(`p`,{children:[`到期时间:`,w(e.expires_at)]}),(0,x.jsxs)(`details`,{className:`source`,children:[(0,x.jsxs)(`summary`,{children:[`记忆依据 · `,e.basis.length,` 条`]}),!e.basis.length&&(0,x.jsx)(`p`,{children:`此条没有模型记忆依据;管理员手写内容由管理员负责核对。`}),e.basis.map((e,n)=>(0,x.jsxs)(`div`,{className:`entry-profile-evidence`,children:[(0,x.jsxs)(`button`,{className:`text-button`,onClick:()=>t(e.memory_id),children:[`记忆 #`,e.memory_id]}),` `,`· 依据修订 `,e.revision,e.evidence?(0,x.jsxs)(x.Fragment,{children:[(0,x.jsx)(`p`,{className:`entry-profile-content`,children:e.evidence.content}),(0,x.jsxs)(`small`,{children:[`依据快照 ·`,` `,w(e.evidence.event_time||e.evidence.created_at),e.evidence.truncated&&` · 摘录已截短`]})]}):(0,x.jsx)(`p`,{className:`muted`,children:`没有保存的依据正文快照。`}),e.memory?(0,x.jsxs)(`details`,{children:[(0,x.jsxs)(`summary`,{children:[`当前记忆 · 修订 `,e.memory.revision]}),(0,x.jsx)(`p`,{children:e.memory.content})]}):(0,x.jsx)(`p`,{className:`muted`,children:`当前记忆已不可用。`})]},n))]})]})}function Jr({value:e}){return(0,x.jsxs)(`p`,{className:`muted`,children:[`v`,e.version,` · `,Pt[e.status]||e.status,` ·`,` `,{periodic:`定期更新`,regenerate:`管理员重新生成`,edit:`管理员编辑`,rollback:`回滚`}[e.source]||e.source,` `,`· 生成于 `,w(e.generated_at),` · 变化程度:`,{small:`小`,medium:`中`,large:`大`}[e.change_degree]||e.change_degree]})}function Yr({value:e,openMemory:t}){let n=e.checks,r=Wr(n.model);return e.cleared_at?(0,x.jsxs)(x.Fragment,{children:[(0,x.jsx)(Jr,{value:e}),(0,x.jsxs)(A,{children:[`因彻底清除而清空 · `,w(e.cleared_at),`。此版本不能回滚。`]})]}):(0,x.jsxs)(x.Fragment,{children:[(0,x.jsx)(Jr,{value:e}),e.invalidated_reason&&(0,x.jsxs)(A,{children:[Ft(e.invalidated_reason),`,此版本不可回滚。`]}),(0,x.jsx)(Kr,{value:e.stale_basis,openMemory:t}),e.sentences.map((e,n)=>(0,x.jsx)(qr,{item:e,openMemory:t},n)),!e.sentences.length&&(0,x.jsx)(`p`,{children:`此版本没有摘要条目。`}),(0,x.jsxs)(`details`,{className:`source`,children:[(0,x.jsx)(`summary`,{children:`检查与删条记录`}),(0,x.jsxs)(`p`,{children:[`检查结果:`,n.passed?`通过`:`未通过`,n.admin_edit&&`(管理员编辑,未调用模型检查)`]}),n.reason&&(0,x.jsx)(`p`,{children:Ft(n.reason)}),n.errors?.map((e,t)=>(0,x.jsx)(`p`,{children:Ft(e)},t)),n.deterministic&&(0,x.jsxs)(`p`,{children:[`规则检查:`,n.deterministic.passed?`通过`:`未通过`,n.deterministic.characters!=null&&` · ${n.deterministic.characters} 字`,n.deterministic.tokens!=null&&` · ${n.deterministic.tokens} token`]}),r?(0,x.jsxs)(x.Fragment,{children:[(0,x.jsxs)(`p`,{children:[`模型检查:`,r.reason]}),r.items.map((e,t)=>(0,x.jsxs)(`div`,{className:`entry-profile-evidence`,children:[(0,x.jsxs)(`strong`,{children:[`检查条目 `,e.index]}),(0,x.jsx)(`p`,{children:n.checked_sentences?.[t]?.text}),(0,x.jsxs)(`p`,{children:[`依据支持:`,e.supported?`是`:`否`,` · 场景限定:`,e.scene_qualified?`充分`:`不足`]}),(0,x.jsx)(`p`,{children:e.reason}),!!e.violations.length&&(0,x.jsxs)(`p`,{children:[`问题:`,e.violations.join(`、`)]})]},e.index))]}):(0,x.jsx)(`p`,{children:n.model==null?`没有模型检查记录。`:`模型检查记录格式无效,无法逐条展示。`}),!!n.deleted_sentences?.length&&(0,x.jsxs)(x.Fragment,{children:[(0,x.jsx)(`h4`,{children:`已删条目`}),n.deleted_sentences.map((e,t)=>(0,x.jsxs)(`div`,{children:[(0,x.jsxs)(`p`,{children:[`第 `,e.index,` 条 ·`,` `,e.stage===`references`?`引用核对`:`模型检查`]}),(0,x.jsx)(`p`,{children:e.sentence?.text||Gr(e.item)}),(0,x.jsx)(`p`,{children:Ft(e.reason)}),!!e.violations?.length&&(0,x.jsx)(`p`,{children:e.violations.join(`、`)})]},t))]}),n.admin_warnings?.map((e,t)=>(0,x.jsxs)(A,{children:[`手写条目 `,e.index,` 的检查提醒:`,e.reason,` ·`,` `,e.violations.join(`、`)]},t)),n.rollback_from!=null&&(0,x.jsxs)(`p`,{children:[`从 v`,n.rollback_from,` 回滚新建。`]})]})]})}function Xr(e){let t=Array.from(e),n=0,r=0,i=0;for(;ie.kind).map(e=>e.text))].sort((e,t)=>t.length-e.length),r=0,i=0;if(n.length){let t=RegExp(`^(?:`+n.map(e=>e.replace(/[.*+?^${}()|[\]\\]/g,`\\$&`)).join(`|`)+`)`,`gm`);for(let n of e.matchAll(t))r+=Xr(e.slice(i,n.index))+1,i=n.index+n[0].length}r+=Xr(e.slice(i));let a=Array.from(e),o=a.filter(e=>/[\u4e00-\u9fff]/u.test(e)).length;return{items:r,characters:a.length,tokens:Math.max(1,o+Math.floor((a.length-o+2)/3))}}function Qr({base:e,initial:t,onSettled:n,view:r}){let[i,a]=(0,l.useState)(t.state===`running`),o=y(`${e}/attempts/${t.id}`,i?1500:0),s=(0,l.useRef)(!1);(0,l.useEffect)(()=>{o.data&&o.data.state!==`running`&&!s.current&&(s.current=!0,a(!1),n())},[o.data,n]);let c=o.data||t;return(0,x.jsxs)(`section`,{className:`entry-profile-section`,"aria-label":`摘要生成任务`,children:[(0,x.jsxs)(`h3`,{children:[`生成任务 #`,c.id]}),o.error&&(0,x.jsxs)(A,{error:!0,children:[o.error,(0,x.jsx)(`button`,{onClick:o.refresh,children:`重试任务状态`})]}),(0,x.jsx)(`p`,{role:`status`,children:c.state===`running`?c.stage===`checking`?`正在检查候选…`:`正在生成候选…`:Pt[c.state]||c.state}),c.reason&&(0,x.jsx)(`p`,{children:Ft(c.reason)}),(0,x.jsxs)(`p`,{className:`fine-print`,children:[`开始:`,w(c.started_at),c.finished_at&&` · 结束:${w(c.finished_at)}`]}),c.state===`rejected`&&(0,x.jsx)(`p`,{children:`当前发布版保持不变。确定性拒绝后自动整理等待新依据;管理员仍可主动重新生成。`}),c.cleared_at&&(0,x.jsx)(`p`,{children:`任务关联内容因彻底清除而清空。`}),c.version!=null&&(0,x.jsxs)(`button`,{onClick:()=>r(c.version),children:[`查看任务版本 v`,c.version]})]})}function $r({subjectId:e,initialVersion:t,openMemory:n}){let r=`/people/${encodeURIComponent(e)}/summary`,i=y(r,5e3),[a,o]=(0,l.useState)(0),[s,c]=(0,l.useState)(t||null),u=y(`${r}/versions?limit=10&offset=${a}`),d=y(i.data?.current?`${r}/versions/${i.data.current.version}`:null),p=y(s?`${r}/versions/${s}`:null),[h,g]=(0,l.useState)(null),b=y(h),[S,C]=(0,l.useState)(`1`),[E,D]=(0,l.useState)(`2`),[O,k]=(0,l.useState)(null),[j,N]=(0,l.useState)(null),[P,F]=(0,l.useState)(null),[I,te]=(0,l.useState)(``),[L,ne]=(0,l.useState)(null),[R,re]=(0,l.useState)(!1),[ie,ae]=(0,l.useState)(``),[oe,se]=(0,l.useState)(``),[ce,le]=(0,l.useState)(!1),ue=(0,l.useRef)(!1),de=(0,l.useRef)(!0);(0,l.useEffect)(()=>(de.current=!0,()=>{de.current=!1}),[]);let z=(0,l.useCallback)(()=>{i.refresh(),u.refresh(),d.refresh(),p.refresh(),b.refresh()},[i.refresh,u.refresh,d.refresh,p.refresh,b.refresh]),B=i.data,V=B?.settings.current_version??0,fe=JSON.stringify(B?.stale_basis||[]);(0,l.useEffect)(()=>{u.refresh(),d.refresh(),p.refresh()},[V,B?.current?.cleared_at,fe,B?.pending?.version,B?.latest_attempt?.state,u.refresh,d.refresh,p.refresh]);let pe=e=>{c(e),ae(``)},me=B?.latest_attempt,he=me&&(!L||me.id>=L.id)?me:L,H=he?.state===`running`,U=R||i.loading||!!i.error||ce,ge=!B?.current||d.data?.version===V&&!d.loading&&!d.error,_e=Zr(O?.content||``,O?.original),ve=B?.edit_limits||{max_items:5,max_chars:200,max_tokens:250},ye=!O?.content.trim()||_e.characters>ve.max_chars||_e.tokens>ve.max_tokens||_e.items>ve.max_items;async function be(e,t,n,i){if(ue.current)return!1;ue.current=!0,re(!0),ae(``),se(``);try{let a=await m(r+e,_(t,n));return de.current&&(e===`/regenerate`&&a.status===202&&a.data.attempt&&ne(a.data.attempt),se(i),F(null),z()),!0}catch(e){if(de.current){let t=e instanceof f&&e.status===409;le(t),ae(t?`当前摘要、设置、依据或生成任务已变化。已刷新版本;请核对后再操作,编辑草稿已保留。`:v(e)),t&&F(null),z()}return!1}finally{ue.current=!1,de.current&&re(!1)}}let xe=(e,t)=>{C(String(e)),D(String(t)),g(`${r}/diff?before_version=${e}&after_version=${t}`)};return(0,x.jsxs)(`section`,{className:`panel person-summary`,"aria-label":`人物摘要`,children:[(0,x.jsxs)(`div`,{className:`panel-heading`,children:[(0,x.jsx)(`h2`,{children:`人物摘要`}),(0,x.jsx)(`button`,{disabled:R,onClick:z,children:`刷新人物摘要`})]}),(0,x.jsx)(`p`,{className:`muted`,children:`从与此人物相关、全部入口共享的有效记忆提炼。查看与轮询不会召回或增加使用次数。`}),(i.error||ie)&&(0,x.jsx)(A,{error:!0,children:ie||i.error}),oe&&(0,x.jsx)(A,{children:oe}),B?(0,x.jsxs)(x.Fragment,{children:[!B.supported&&(0,x.jsx)(A,{children:`此主体类型或已合并人物不支持摘要写操作,可查阅已有历史。`}),!B.global_enabled&&(0,x.jsx)(A,{children:`全局人物摘要开关已关闭,不会生成或加入回复准备。`}),ce&&(0,x.jsx)(`button`,{disabled:R||i.loading||!!i.error||!ge,onClick:()=>{le(!1),ae(``),k(e=>e?{...e,expected_version:V,original:d.data?.sentences||[]}:null),N(e=>e?{...e,expected_version:V}:null)},children:`已核对最新版本,继续操作`}),(0,x.jsxs)(`p`,{children:[(0,x.jsx)(M,{children:B.settings.enabled?`此人摘要已开启`:`此人摘要已关闭`}),` `,`·`,` `,B.settings.publish_mode===`all_manual`?`全部人工确认`:`检查通过自动发布`,` `,`·`,` `,(0,x.jsxs)(M,{tone:B.pending_count?`amber`:``,children:[`待确认 `,B.pending_count]})]}),B.supported&&(0,x.jsxs)(`div`,{className:`button-row`,children:[(0,x.jsx)(`button`,{disabled:U||!ge,onClick:()=>k({content:B.current?.content||``,expected_version:V,original:d.data?.sentences||[]}),children:`编辑摘要`}),(0,x.jsx)(`button`,{disabled:U,onClick:()=>N({enabled:B.settings.enabled,publish_mode:B.settings.publish_mode,expected_version:V}),children:`人物摘要设置`}),(0,x.jsx)(`button`,{disabled:U||H||!B.global_enabled||!B.settings.enabled,onClick:()=>void be(`/regenerate`,`POST`,{expected_version:V},`已接受重新生成,请查看任务进度。`),children:H?`摘要生成中…`:`重新生成摘要`})]}),(0,x.jsx)(`p`,{className:`fine-print`,children:`重新生成至少需要 8 条相关全局有效记忆,并受开关、版本检查与模型预算限制。`}),j&&(0,x.jsxs)(`form`,{className:`entry-profile-section`,"aria-label":`人物摘要设置`,onSubmit:async e=>{e.preventDefault(),!(U||j.expected_version!==V)&&await be(`/settings`,`PATCH`,j,`人物摘要设置已保存`)&&N(null)},children:[(0,x.jsxs)(`label`,{className:`check-label`,children:[(0,x.jsx)(`input`,{type:`checkbox`,disabled:R,checked:j.enabled,onChange:e=>N({...j,enabled:e.target.checked})}),`启用此人的摘要`]}),(0,x.jsxs)(`label`,{children:[`摘要发布方式`,(0,x.jsxs)(`select`,{disabled:R,value:j.publish_mode,onChange:e=>N({...j,publish_mode:e.target.value}),children:[(0,x.jsx)(`option`,{value:`check_auto`,children:`检查通过自动发布`}),(0,x.jsx)(`option`,{value:`all_manual`,children:`全部人工确认`})]})]}),(0,x.jsx)(`p`,{children:`关闭后保留历史;发布方式只影响之后的候选,不会发布已有待确认版本。`}),j.expected_version!==V&&(0,x.jsx)(A,{children:`设置基于旧版本,请取消后重新打开设置。`}),(0,x.jsxs)(`div`,{className:`button-row`,children:[(0,x.jsx)(`button`,{disabled:U||j.expected_version!==V,children:`保存人物摘要设置`}),(0,x.jsx)(`button`,{type:`button`,disabled:R,onClick:()=>N(null),children:`取消设置`})]})]}),O&&(0,x.jsxs)(`form`,{className:`entry-profile-section`,"aria-label":`编辑人物摘要`,onSubmit:async e=>{e.preventDefault(),!(U||ye||O.expected_version!==V)&&await be(``,`PUT`,{content:O.content,expected_version:O.expected_version},`摘要编辑已发布`)&&k(null)},children:[(0,x.jsx)(`h3`,{children:`编辑并直接发布`}),(0,x.jsx)(`p`,{children:`新增或改动条目标为管理员手写,之后生成会保留;未改条目保留原依据。按句号或换行分条。`}),(0,x.jsxs)(`label`,{children:[`人物摘要正文`,(0,x.jsx)(`textarea`,{rows:6,disabled:R,value:O.content,onChange:e=>k({...O,content:e.target.value})})]}),(0,x.jsxs)(`p`,{children:[_e.items,` / `,ve.max_items,` 条 · `,_e.characters,` /`,` `,ve.max_chars,` 字 · `,_e.tokens,` / `,ve.max_tokens,` `,`估算 token(无最低字数) · 基于 v`,O.expected_version]}),O.expected_version!==V&&(0,x.jsxs)(A,{children:[`当前版本已变化,草稿已保留。请核对最新摘要后更新草稿版本。`,(0,x.jsx)(`button`,{type:`button`,disabled:!ge||U,onClick:()=>k({...O,expected_version:V,original:d.data?.sentences||[]}),children:`更新草稿版本`})]}),(0,x.jsxs)(`div`,{className:`button-row`,children:[(0,x.jsx)(`button`,{disabled:U||ye||O.expected_version!==V,children:`发布摘要编辑`}),(0,x.jsx)(`button`,{type:`button`,disabled:R,onClick:()=>k(null),children:`取消编辑摘要`})]})]}),(0,x.jsxs)(`section`,{className:`entry-profile-section`,"aria-label":`当前人物摘要`,children:[(0,x.jsx)(`h3`,{children:`当前摘要`}),B.current?d.error?(0,x.jsxs)(A,{error:!0,children:[d.error,(0,x.jsx)(`button`,{onClick:d.refresh,children:`重试当前摘要`})]}):!d.data||d.data.version!==V?(0,x.jsx)(`p`,{children:`正在读取当前摘要…`}):(0,x.jsx)(Yr,{value:d.data,openMemory:n}):(0,x.jsx)(`p`,{children:`尚无已发布的人物摘要。`}),!d.data&&(0,x.jsx)(Kr,{value:B.stale_basis,openMemory:n})]}),B.pending&&(0,x.jsxs)(`section`,{className:`entry-profile-section`,"aria-label":`待确认人物摘要`,children:[(0,x.jsxs)(`h3`,{children:[`待确认候选 · v`,B.pending.version]}),(0,x.jsx)(`p`,{className:`entry-profile-content`,children:B.pending.content}),(0,x.jsxs)(`div`,{className:`button-row`,children:[(0,x.jsxs)(`button`,{onClick:()=>pe(B.pending.version),children:[`审阅候选 v`,B.pending.version]}),B.current&&(0,x.jsx)(`button`,{onClick:()=>xe(V,B.pending.version),children:`对比当前与候选`})]})]}),he&&(0,x.jsx)(Qr,{base:r,initial:he,onSettled:z,view:pe},he.id),(0,x.jsxs)(`section`,{className:`entry-profile-section`,"aria-label":`人物摘要历史`,children:[(0,x.jsx)(`h3`,{children:`版本历史`}),u.error&&(0,x.jsxs)(A,{error:!0,children:[u.error,(0,x.jsx)(`button`,{onClick:u.refresh,children:`重试摘要历史`})]}),u.data?.items.map(e=>(0,x.jsxs)(`div`,{className:`entry-profile-evidence`,children:[(0,x.jsxs)(`button`,{onClick:()=>pe(e.version),children:[`查看摘要 v`,e.version]}),` `,(0,x.jsx)(M,{children:e.is_current?`当前版本`:Pt[e.status]||e.status}),e.cleared_at&&(0,x.jsx)(M,{tone:`warning`,children:`因彻底清除而清空`}),(0,x.jsx)(`p`,{children:w(e.generated_at)})]},e.version)),u.data&&!u.data.total&&(0,x.jsx)(`p`,{children:`暂无历史版本。`}),u.data&&(0,x.jsx)(T,{total:u.data.total,offset:a,change:o,limit:10})]}),s!=null&&(0,x.jsxs)(`section`,{className:`entry-profile-section`,"aria-label":`人物摘要版本详情`,children:[(0,x.jsxs)(`h3`,{children:[`摘要 v`,s,` 的依据与检查`]}),p.error?(0,x.jsxs)(A,{error:!0,children:[p.error,(0,x.jsx)(`button`,{onClick:p.refresh,children:`重试摘要版本`})]}):p.data?(0,x.jsxs)(x.Fragment,{children:[(0,x.jsx)(Yr,{value:p.data,openMemory:n}),B.supported&&p.data.status===`candidate`&&(0,x.jsxs)(`div`,{className:`button-row`,children:[(0,x.jsxs)(`button`,{disabled:U||!!p.data.cleared_at||!p.data.checks.passed,onClick:()=>F({kind:`confirm`,version:s,expected_version:V}),children:[`确认发布摘要 v`,s]}),(0,x.jsxs)(`button`,{disabled:U,onClick:()=>{te(``),F({kind:`reject`,version:s,expected_version:V})},children:[`拒绝摘要 v`,s]})]}),B.supported&&p.data.status===`published`&&!p.data.is_current&&(0,x.jsxs)(`button`,{disabled:U||!!p.data.cleared_at||!!p.data.invalidated_reason||!p.data.content.trim(),onClick:()=>F({kind:`rollback`,version:s,expected_version:V}),children:[`回滚到摘要 v`,s]})]}):(0,x.jsx)(`p`,{children:`正在读取版本…`})]}),(0,x.jsxs)(`section`,{className:`entry-profile-section`,"aria-label":`摘要版本对比`,children:[(0,x.jsx)(`h3`,{children:`任意两版对比`}),(0,x.jsxs)(`form`,{className:`button-row`,onSubmit:e=>{e.preventDefault(),Number.isSafeInteger(+S)&&+S>0&&Number.isSafeInteger(+E)&&+E>0&&xe(+S,+E)},children:[(0,x.jsxs)(`label`,{children:[`较早版本`,(0,x.jsx)(`input`,{type:`number`,min:1,required:!0,value:S,onChange:e=>C(e.target.value)})]}),(0,x.jsxs)(`label`,{children:[`较晚版本`,(0,x.jsx)(`input`,{type:`number`,min:1,required:!0,value:E,onChange:e=>D(e.target.value)})]}),(0,x.jsx)(`button`,{children:`对比摘要版本`})]}),b.error&&(0,x.jsxs)(A,{error:!0,children:[b.error,(0,x.jsx)(`button`,{onClick:b.refresh,children:`重试摘要对比`})]}),b.data&&(0,x.jsxs)(x.Fragment,{children:[(0,x.jsxs)(`p`,{children:[`v`,b.data.before,` → v`,b.data.after,!b.data.changed&&` · 条目没有变化`]}),b.data.sentences.map((e,t)=>(0,x.jsxs)(`div`,{className:`persona-diff-row`,children:[(0,x.jsx)(`strong`,{children:{equal:`保留`,replace:`修改`,insert:`新增`,delete:`删除`}[e.op]||e.op}),(0,x.jsxs)(`div`,{className:`persona-diff-columns`,children:[(0,x.jsxs)(`div`,{children:[(0,x.jsx)(`small`,{children:`之前`}),e.before.map((e,t)=>(0,x.jsx)(`p`,{className:`entry-profile-content`,children:e.text},t))]}),(0,x.jsxs)(`div`,{children:[(0,x.jsx)(`small`,{children:`之后`}),e.after.map((e,t)=>(0,x.jsx)(`p`,{className:`entry-profile-content`,children:e.text},t))]})]})]},t))]})]}),(0,x.jsx)(`a`,{href:`#/operations?object_type=person_summary&object_id=${encodeURIComponent(e)}`,children:`查看人物摘要操作记录`}),P&&(0,x.jsxs)(ee,{title:P.kind===`confirm`?`确认发布人物摘要`:P.kind===`reject`?`拒绝人物摘要`:`回滚人物摘要`,onClose:()=>F(null),closeDisabled:R,children:[(0,x.jsx)(`p`,{children:P.kind===`rollback`?`将复制 v${P.version},新建并发布一个版本;历史版本保持原记录。`:P.kind===`confirm`?`将发布候选 v${P.version},替换当前摘要。`:`将拒绝候选 v${P.version},保留当前发布版。`}),P.kind===`reject`&&(0,x.jsxs)(`label`,{children:[`拒绝摘要原因`,(0,x.jsx)(`input`,{maxLength:1e3,value:I,onChange:e=>te(e.target.value),disabled:R})]}),(0,x.jsx)(`p`,{children:`请核对条目、依据和检查记录后再次确认。`}),ie&&(0,x.jsx)(A,{error:!0,children:ie}),(0,x.jsxs)(`div`,{className:`button-row`,children:[(0,x.jsx)(`button`,{disabled:R,onClick:()=>F(null),children:`取消操作`}),(0,x.jsxs)(`button`,{className:`primary`,disabled:U||P.expected_version!==V,onClick:()=>void be(`/versions/${P.version}/${P.kind}`,`POST`,{expected_version:P.expected_version,...P.kind===`reject`?{reason:I.trim()||`管理员拒绝`}:{}},P.kind===`rollback`?`已新建并发布回滚版本`:P.kind===`confirm`?`候选已确认发布`:`候选已拒绝`),children:[`确认`,P.kind===`rollback`?`回滚`:P.kind===`confirm`?`发布`:`拒绝`]})]})]})]}):(0,x.jsx)(`p`,{role:`status`,children:`正在读取人物摘要…`})]})}function ei({initialQuery:e=``,onChange:t,openMemory:n=()=>{}}){let r=new URLSearchParams(e),[i,a]=(0,l.useState)(r.get(`id`)),[o,s]=(0,l.useState)(``),[c,u]=(0,l.useState)({text:``,pending_only:r.get(`pending_only`)===`true`,include_merged:!1}),[d,f]=(0,l.useState)(c),[p,m]=(0,l.useState)(0),h=new URLSearchParams({text:d.text,pending_only:String(d.pending_only),include_merged:String(d.include_merged),limit:`30`,offset:String(p)}),g=y(i?null:`/people?${h}`,5e3);return(0,x.jsxs)(x.Fragment,{children:[(0,x.jsxs)(`div`,{className:`page-heading`,children:[(0,x.jsxs)(`div`,{children:[(0,x.jsx)(`p`,{className:`eyebrow`,children:`名字背后的人`}),(0,x.jsx)(`h1`,{children:`人物`}),(0,x.jsx)(`p`,{children:`核对身份与证据,再决定是否为同一个人。`})]}),(0,x.jsx)(`a`,{className:`button`,href:`#/people-graph`,children:`人物关系图 →`})]}),o&&(0,x.jsx)(A,{children:o}),i?(0,x.jsx)(ni,{id:i,summaryVersion:i===r.get(`id`)&&Number(r.get(`summary_version`))||void 0,openMemory:n,openPerson:a,onBack:()=>a(null),onChange:()=>{g.refresh(),t()},onMerged:e=>{s(`已确认联系并完成人物合并,以下为保留人物的资料。`),a(e)}},i):(0,x.jsxs)(x.Fragment,{children:[(0,x.jsxs)(`form`,{className:`panel people-filters`,onSubmit:e=>{e.preventDefault(),f({...c}),m(0),g.refresh()},children:[(0,x.jsxs)(`label`,{children:[`搜索名字或别名`,(0,x.jsx)(`input`,{maxLength:100,value:c.text,onChange:e=>u({...c,text:e.target.value})})]}),(0,x.jsxs)(`label`,{className:`check-label`,children:[(0,x.jsx)(`input`,{type:`checkbox`,checked:c.pending_only,onChange:e=>u({...c,pending_only:e.target.checked})}),`只看待确认`]}),(0,x.jsxs)(`label`,{className:`check-label`,children:[(0,x.jsx)(`input`,{type:`checkbox`,checked:c.include_merged,onChange:e=>u({...c,include_merged:e.target.checked})}),`包含已合并人物`]}),(0,x.jsx)(`button`,{className:`primary`,children:`搜索人物`})]}),g.error&&(0,x.jsxs)(A,{error:!0,children:[g.error,(0,x.jsx)(`button`,{onClick:g.refresh,children:`重新加载`})]}),g.data?(0,x.jsxs)(x.Fragment,{children:[(0,x.jsxs)(`p`,{className:`muted`,children:[`共 `,g.data.total,` 位人物`]}),!g.data.items.length&&(0,x.jsx)(j,{title:`当前范围没有人物`}),(0,x.jsx)(`div`,{className:`entry-grid`,children:g.data.items.map(e=>(0,x.jsxs)(`article`,{className:`panel person-card`,children:[(0,x.jsx)(`h2`,{children:e.name}),(0,x.jsxs)(`small`,{className:`muted`,children:[`人物标识:`,e.id]}),!!e.pending_links&&(0,x.jsx)(`p`,{children:(0,x.jsxs)(M,{tone:`purple`,children:[`可能是同一人 · `,e.pending_links,` 条待确认`]})}),e.merged_into&&(0,x.jsx)(`p`,{children:(0,x.jsx)(M,{children:`已合并`})}),(0,x.jsxs)(`p`,{children:[`别名:`,e.aliases.map(e=>e.alias).join(`、`)||`暂无`]}),(0,x.jsxs)(`p`,{children:[`相关记忆 `,e.memory_count,` 条`]}),(0,x.jsx)(`button`,{className:`text-button`,onClick:()=>a(e.id),"aria-label":`查看人物 ${e.name}`,children:`查看人物 →`})]},e.id))}),(0,x.jsx)(T,{total:g.data.total,offset:p,change:m})]}):(0,x.jsx)(`p`,{role:`status`,children:`正在读取人物…`})]})]})}function ti({messages:e}){return(0,x.jsxs)(`details`,{className:`person-evidence`,open:!0,children:[(0,x.jsxs)(`summary`,{children:[`证据消息 · `,e.length,` 条`]}),!e.length&&(0,x.jsx)(`p`,{className:`muted`,children:`没有可展示的证据消息。`}),e.map(e=>(0,x.jsxs)(`blockquote`,{children:[(0,x.jsxs)(`small`,{children:[e.sender_name||e.sender_subject_id,` ·`,` `,e.entry_name||e.entry_id,` · #`,e.id,` ·`,` `,w(e.occurred_at||e.received_at)]}),(0,x.jsx)(`p`,{children:e.content}),e.quote_content&&(0,x.jsxs)(`div`,{className:`evidence-quote`,children:[(0,x.jsxs)(`small`,{children:[`引用`,e.quote_author_subject_id?` · 作者标识 ${e.quote_author_subject_id}`:``]}),(0,x.jsx)(`p`,{children:e.quote_content})]})]},e.id))]})}function ni({id:e,summaryVersion:t,openMemory:n,openPerson:r,onBack:i,onChange:a,onMerged:o}){let s=y(`/people/${encodeURIComponent(e)}`),[c,u]=(0,l.useState)(!1),[d,p]=(0,l.useState)(``),[m,h]=(0,l.useState)(null),[b,S]=(0,l.useState)(``),[C,w]=(0,l.useState)(``),[T,D]=(0,l.useState)(!1),[O,k]=(0,l.useState)(!1),j=s.data,N=T||O||s.loading||!!s.error,P=()=>{h(null),S(``),k(!1),s.refresh()},F=e=>{let t=e instanceof f&&e.status===409;k(t),S(t?`资料或联系已变化,请刷新人物资料后重新核对。`:v(e))},I=e=>{h(null),w(e),S(``),s.refresh(),a()};async function te(t){if(t.preventDefault(),!j||N)return;let n=d.trim();if(!n||Array.from(n).length>100){S(`别名需为 1 至 100 个字符`);return}if(n.toLocaleLowerCase()===j.name.trim().toLocaleLowerCase()){S(`别名不能与人物名字相同`);return}D(!0),S(``),w(``);try{await g(`/people/${encodeURIComponent(e)}/aliases`,_(`POST`,{alias:n,expected_revision:j.revision})),p(``),I(`已添加别名`)}catch(e){F(e)}finally{D(!1)}}async function L(){if(j&&m&&m.kind!==`merge`&&!N){D(!0),S(``),w(``);try{m.kind===`deny`?await g(`/people/links/${m.link.id}/deny`,_(`POST`,{expected_revision:m.link.revision})):await g(`/people/${encodeURIComponent(e)}/aliases/${m.alias.id}`,_(`DELETE`,{expected_revision:j.revision})),I(m.kind===`deny`?`联系已否认,后续学习不会重新提出这对人物的同一人联系。`:`已删除别名,后续学习不会自动加回。必要时可手动添加。`)}catch(e){F(e)}finally{D(!1)}}}return(0,x.jsxs)(`section`,{className:`person-detail`,children:[(0,x.jsxs)(`div`,{className:`button-row`,children:[(0,x.jsx)(`button`,{className:`text-button`,onClick:i,children:`← 返回人物列表`}),(0,x.jsx)(`button`,{disabled:T||s.loading,onClick:P,children:`刷新人物资料`})]}),(s.error||b)&&!m&&(0,x.jsx)(A,{error:!0,children:b||s.error}),C&&(0,x.jsx)(A,{children:C}),j?(0,x.jsxs)(x.Fragment,{children:[c&&(0,x.jsx)(Pr,{scope:{subject_id:j.id},name:j.name,onClose:()=>u(!1),onChanged:()=>{s.refresh(),a()}}),(0,x.jsxs)(`section`,{className:`panel`,children:[(0,x.jsx)(`h2`,{children:j.name}),!j.merged_into&&(0,x.jsx)(`button`,{className:`secondary`,disabled:N,onClick:()=>u(!0),children:`批量遗忘/删除`}),(0,x.jsxs)(`p`,{className:`muted`,children:[`人物标识:`,j.id,` · 资料修订 `,j.revision]}),j.merged_into&&(0,x.jsxs)(A,{children:[`此人物已合并,原标识保留用于追溯。`,(0,x.jsx)(`button`,{onClick:()=>r(j.canonical_id),children:`查看合并后的人物`})]}),(0,x.jsx)($r,{subjectId:e,initialVersion:t,openMemory:n}),(0,x.jsx)(`h3`,{children:`平台身份`}),!j.platform_identities.length&&(0,x.jsx)(`p`,{className:`muted`,children:`暂无平台身份。`}),j.platform_identities.map(e=>(0,x.jsxs)(`dl`,{className:`identity-facts`,children:[(0,x.jsxs)(`div`,{children:[(0,x.jsx)(`dt`,{children:`平台`}),(0,x.jsx)(`dd`,{children:e.platform===`iris-trial`?`试用`:e.platform})]}),(0,x.jsxs)(`div`,{children:[(0,x.jsx)(`dt`,{children:`账号`}),(0,x.jsx)(`dd`,{children:e.account_id})]}),(0,x.jsxs)(`div`,{children:[(0,x.jsx)(`dt`,{children:`显示名`}),(0,x.jsx)(`dd`,{children:e.display_name})]})]},`${e.platform}:${e.account_id}`)),(0,x.jsxs)(`h3`,{children:[`相关记忆 · `,j.memory_count,` 条`]}),(0,x.jsx)(`p`,{children:Object.entries(j.memory_counts).map(([e,t])=>`${E(e)} ${t} 条`).join(` · `)||`暂无相关记忆`}),(0,x.jsx)(`a`,{href:`#/memories?${new URLSearchParams({person_id:j.canonical_id,lifecycle:`all`})}`,children:`查看关于此人的记忆`})]}),(0,x.jsxs)(`section`,{className:`panel`,children:[(0,x.jsx)(`h3`,{children:`别名`}),!j.aliases.length&&(0,x.jsx)(`p`,{className:`muted`,children:`暂无别名。`}),(0,x.jsx)(`ul`,{className:`alias-list`,children:j.aliases.map(e=>(0,x.jsxs)(`li`,{children:[(0,x.jsx)(`span`,{children:e.alias}),!!e.evidence_message_ids.length&&(0,x.jsxs)(`small`,{className:`muted`,children:[`证据消息`,` `,e.evidence_message_ids.map(e=>`#${e}`).join(`、`)]}),!j.merged_into&&(0,x.jsx)(`button`,{disabled:N,className:`text-button danger-text`,"aria-label":`删除别名 ${e.alias}`,onClick:()=>{S(``),h({kind:`alias`,alias:e})},children:`删除`})]},e.id))}),!j.merged_into&&(0,x.jsxs)(`form`,{className:`inline-form`,onSubmit:te,noValidate:!0,children:[(0,x.jsxs)(`label`,{children:[`新别名`,(0,x.jsx)(`input`,{value:d,maxLength:100,disabled:N,onChange:e=>p(e.target.value)})]}),(0,x.jsx)(`button`,{disabled:N,className:`secondary`,children:`添加别名`})]})]}),(0,x.jsxs)(`section`,{className:`panel`,children:[(0,x.jsx)(`h3`,{children:`可能是同一人`}),!j.same_as.length&&(0,x.jsx)(`p`,{className:`muted`,children:`暂无身份联系。`}),j.same_as.map(t=>(0,x.jsxs)(`article`,{className:`person-link`,children:[(0,x.jsx)(`h4`,{children:t.subjects.map(e=>e.name).join(` ↔ `)}),(0,x.jsxs)(`p`,{children:[(0,x.jsx)(M,{tone:t.status===`possible`?`purple`:``,children:t.status===`possible`?`待确认`:t.status===`denied`?`已否认`:`已确认`}),` `,`相信程度 `,t.belief,` / 100`]}),(0,x.jsx)(`div`,{className:`button-row`,children:t.subjects.filter(t=>t.id!==e).map(e=>(0,x.jsxs)(`button`,{className:`text-button`,onClick:()=>r(e.id),children:[`查看 `,e.name,` 的资料`]},e.id))}),(0,x.jsx)(ti,{messages:t.evidence_messages}),t.status===`possible`&&!j.merged_into&&(0,x.jsxs)(`div`,{className:`button-row`,children:[(0,x.jsx)(`button`,{disabled:N,className:`primary`,onClick:()=>{S(``),h({kind:`merge`,link:t})},children:`确认并合并`}),(0,x.jsx)(`button`,{disabled:N,onClick:()=>{S(``),h({kind:`deny`,link:t})},children:`否认联系`})]})]},t.id))]}),(0,x.jsxs)(`section`,{className:`panel`,children:[(0,x.jsx)(`h3`,{children:`扮演关系`}),!j.roleplay.length&&(0,x.jsx)(`p`,{className:`muted`,children:`暂无扮演关系。`}),j.roleplay.map(e=>(0,x.jsxs)(`article`,{className:`person-link`,children:[(0,x.jsxs)(`h4`,{children:[e.actor.name,` 扮演 `,e.character.name]}),(0,x.jsxs)(`p`,{children:[(0,x.jsx)(M,{children:`虚构角色`}),` · 相信程度 `,e.belief,` / 100`]}),(0,x.jsxs)(`p`,{children:[`所属场景:`,e.worlds.map(e=>e??`场景未知`).join(`、`)]}),(0,x.jsx)(`p`,{className:`muted`,children:`扮演关系不代表同一人,角色经历不归入现实人物。`}),(0,x.jsx)(ti,{messages:e.evidence_messages})]},e.id))]}),m?.kind===`merge`?(0,x.jsx)(ri,{link:m.link,initialTarget:e,onClose:()=>h(null),onRefresh:P,onSuccess:e=>{I(``),o(e)}}):m&&(0,x.jsxs)(ee,{title:m.kind===`deny`?`否认人物联系`:`删除别名`,onClose:()=>h(null),closeDisabled:T,children:[m.kind===`deny`?(0,x.jsxs)(`p`,{children:[`否认 `,m.link.subjects.map(e=>e.name).join(`与`),` `,`是同一人。否认后,后续学习不会改回待确认,也不会再次提出这对人物的同一人联系。`]}):(0,x.jsxs)(`p`,{children:[`删除别名“`,m.alias.alias,`”及其别名证据记录,后续学习不会自动加回。管理员仍可手动添加。`]}),b&&(0,x.jsxs)(A,{error:!0,children:[b,O&&(0,x.jsx)(`button`,{onClick:P,children:`刷新人物资料`})]}),(0,x.jsxs)(`div`,{className:`button-row`,children:[(0,x.jsx)(`button`,{onClick:()=>h(null),disabled:T,children:`取消`}),(0,x.jsx)(`button`,{className:`danger-button`,disabled:N,onClick:L,children:m.kind===`deny`?`确认否认`:`确认删除别名`})]})]})]}):(0,x.jsx)(`p`,{role:`status`,children:`正在读取人物资料…`})]})}function ri({link:e,initialTarget:t,onClose:n,onRefresh:r,onSuccess:i}){let a=y(`/people/${encodeURIComponent(e.subjects[0].id)}`),o=y(`/people/${encodeURIComponent(e.subjects[1].id)}`),[s,c]=(0,l.useState)(t),[u,d]=(0,l.useState)(!1),[p,m]=(0,l.useState)(!1),[h,b]=(0,l.useState)(!1),[S,C]=(0,l.useState)(``),w=a.data&&o.data?[a.data,o.data]:null,T=w?.find(e=>e.id===s),E=w?.find(e=>e.id!==s),D=!!a.error||!!o.error||a.loading||o.loading||w?.some(e=>!!e.merged_into);async function O(){if(T&&E&&!p&&!h&&!D&&u){m(!0),C(``);try{i((await g(`/people/links/${e.id}/confirm`,_(`POST`,{target_id:T.id,expected_revision:e.revision,expected_source_revision:E.revision,expected_target_revision:T.revision}))).target_id)}catch(e){let t=e instanceof f&&e.status===409;b(t),C(t?`资料或联系已变化,请刷新人物资料后重新核对。`:v(e))}finally{m(!1)}}}return(0,x.jsxs)(ee,{title:`确认并合并人物`,onClose:n,closeDisabled:p,children:[(S||a.error||o.error)&&(0,x.jsxs)(A,{error:!0,children:[S||a.error||o.error,(0,x.jsx)(`button`,{disabled:p,onClick:r,children:`刷新人物资料`})]}),w?(0,x.jsxs)(x.Fragment,{children:[w.some(e=>e.merged_into)&&(0,x.jsxs)(A,{error:!0,children:[`人物已合并,请刷新后核对。`,(0,x.jsx)(`button`,{onClick:r,children:`刷新人物资料`})]}),(0,x.jsxs)(`fieldset`,{className:`lifecycle-fieldset`,disabled:p||h||!!D,children:[(0,x.jsxs)(`label`,{children:[`保留的人物`,(0,x.jsx)(`select`,{value:s,onChange:e=>{c(e.target.value),d(!1)},children:w.map(e=>(0,x.jsxs)(`option`,{value:e.id,children:[e.name,` · `,e.id]},e.id))})]}),(0,x.jsxs)(`div`,{className:`purge-confirm`,children:[(0,x.jsxs)(`strong`,{children:[`将“`,E?.name,`”合并到“`,T?.name,`”`]}),(0,x.jsx)(`p`,{children:`平台身份、有效别名、记忆的涉及人和说话人、人物联系及从属人物会转到保留方。被合并方的名字成为别名,原人物保留为合并占位。`}),(0,x.jsx)(`p`,{children:`记忆正文、修订号与分数保持原值。联系冲突时否认优先。M2 不提供撤销合并,请先核对双方账号与证据。`}),(0,x.jsxs)(`p`,{children:[E?.name,`:`,E?.platform_identities.length,` `,`个平台身份,`,E?.memory_count,` 条相关记忆。`]}),(0,x.jsxs)(`p`,{children:[T?.name,`:`,T?.platform_identities.length,` 个平台身份,`,T?.memory_count,` 条相关记忆。`]}),w.map(e=>(0,x.jsxs)(`div`,{className:`merge-identity`,children:[(0,x.jsxs)(`strong`,{children:[e.name,` · `,e.id]}),(0,x.jsxs)(`p`,{children:[`账号:`,e.platform_identities.map(e=>`${e.platform} / ${e.account_id}`).join(`;`)||`未绑定平台账号`]}),(0,x.jsxs)(`p`,{children:[`别名:`,e.aliases.map(e=>e.alias).join(`、`)||`暂无`]})]},e.id)),(0,x.jsxs)(`label`,{className:`check-label`,children:[(0,x.jsx)(`input`,{type:`checkbox`,checked:u,onChange:e=>d(e.target.checked)}),`我已核对身份,理解合并后无法在界面撤销`]})]})]}),(0,x.jsxs)(`div`,{className:`button-row`,children:[(0,x.jsx)(`button`,{disabled:p,onClick:n,children:`取消`}),(0,x.jsx)(`button`,{className:`danger-button`,disabled:p||h||!!D||!u,onClick:O,children:p?`正在合并…`:`确认合并`})]})]}):(0,x.jsx)(`p`,{role:`status`,children:`正在核对双方资料…`})]})}var ii=e=>JSON.stringify([e.source,e.target]),ai=e=>`${e.links.some(e=>e.kind===`same_as`)?`可能同一人`:e.links.some(e=>e.kind===`roleplay`)?`扮演`:e.relationship_count?`关系`:`共现`} · ${e.memory_count}`;function oi(e,t=900){let n=t<560,r=n?48:88,i=n?104:180,a=n?110:135,o=n?24:36,s=n?64:80,c=Math.max(1,Math.ceil(Math.sqrt(e*(n?1:1.7)))),l=Math.max(260,t,e>12?r*2+(c-1)*i:0),u=Math.min(c,Math.max(1,Math.floor((l-r*2)/i)+1)),d=Math.max(1,Math.ceil(e/u));return{width:l,height:Math.max(e<=3?220:n?330:400,o+s+(d-1)*a),compact:n,marginX:r,top:o,bottom:s,rows:d}}function si(e,t,n=900){if(!e.length)return{};let r=oi(e.length,n),i=e.map((t,n)=>({x:200*Math.cos(n*2*Math.PI/e.length),y:200*Math.sin(n*2*Math.PI/e.length)})),a=new Map(e.map((e,t)=>[e.id,t]));for(let e=0;e<90;e++){let n=i.map(e=>({x:-e.x*.009,y:-e.y*.009}));for(let e=0;e{e.x+=Math.max(-r,Math.min(r,n[t].x)),e.y+=Math.max(-r,Math.min(r,n[t].y))})}let o=[];for(let t=0;te.x),c=i.map(e=>e.y),l=Math.min(...s),u=Math.min(...c),d=Math.max(...s)-l,f=Math.max(...c)-u;return Object.fromEntries(e.map((e,t)=>{let n=d>.001?r.marginX+(i[t].x-l)/d*(r.width-r.marginX*2):r.width/2,a=f>.001?r.top+(i[t].y-u)/f*(r.height-r.top-r.bottom):(r.top+r.height-r.bottom)/2,s=0;for(let e=1;ee.leftt.left&&e.topt.top;function li(e,t,n,r){let i=e.map(e=>{let t=n[e.id];return{left:t.x-(r.compact?48:84),right:t.x+(r.compact?48:84),top:t.y-(r.compact?22:33),bottom:t.y+(r.compact?57:70)}}),a={};for(let e of t){let t=n[e.source],o=n[e.target],s=o.x-t.x,c=o.y-t.y,l=Math.max(1,Math.hypot(s,c)),u=r.compact?11:12,d=Array.from(ai(e)).reduce((e,t)=>e+u*(t.charCodeAt(0)>255?1:.65),12),f=u+10,p=!1;for(let n of[.5,.35,.65,.2,.8]){for(let o of[-16,16,-32,32,-48,48]){let u={x:t.x+s*n-c/l*o,y:t.y+c*n+s/l*o},m={left:u.x-d/2,right:u.x+d/2,top:u.y-f/2,bottom:u.y+f/2};if(!(m.left<4||m.right>r.width-4||m.top<4||m.bottom>r.height-4||i.some(e=>ci(m,e)))){a[ii(e)]=u,i.push(m),p=!0;break}}if(p)break}}return a}var ui=(e,t)=>Array.from(e).length>t?Array.from(e).slice(0,t).join(``)+`…`:e,di=e=>`#/memories?person_id=${encodeURIComponent(e)}&lifecycle=active`,fi=e=>new URLSearchParams(Object.entries(e).filter(([,e])=>e)).toString();function pi({edge:e}){return(0,x.jsxs)(`span`,{className:`graph-kinds`,children:[e.memory_count>0&&(0,x.jsxs)(M,{children:[`共同涉及 `,e.memory_count]}),e.relationship_count>0&&(0,x.jsxs)(M,{tone:`purple`,children:[`关系记忆 `,e.relationship_count]}),e.links.some(e=>e.kind===`same_as`)&&(0,x.jsx)(M,{tone:`purple`,children:`可能同一人 · 未确认`}),e.links.some(e=>e.kind===`roleplay`)&&(0,x.jsx)(M,{children:`扮演 · 虚构角色`})]})}function mi({data:e,onEdge:t}){let n=(0,l.useRef)(null),[r,i]=(0,l.useState)(900),[a,o]=(0,l.useState)(null),[s,c]=(0,l.useState)(null),u=(0,l.useId)();(0,l.useEffect)(()=>{let e=n.current;if(!e)return;let t=()=>{let t=Math.floor(e.getBoundingClientRect().width);t>0&&i(t)};if(t(),typeof ResizeObserver>`u`)return;let r=new ResizeObserver(t);return r.observe(e),()=>r.disconnect()},[]);let d=(0,l.useMemo)(()=>oi(e.nodes.length,r),[e.nodes.length,r]),f=(0,l.useMemo)(()=>si(e.nodes,e.edges,r),[e,r]),p=(0,l.useMemo)(()=>li(e.nodes,e.edges,f,d),[e,f,d]),m=new Map(e.nodes.map(e=>[e.id,e.name])),h=e.edges.find(e=>ii(e)===(s||a));return(0,x.jsxs)(x.Fragment,{children:[(0,x.jsx)(`div`,{ref:n,className:`graph-scroll`,tabIndex:0,"aria-label":`关系图画布,可横向滚动`,children:(0,x.jsxs)(`svg`,{viewBox:`0 0 ${d.width} ${d.height}`,style:{minWidth:d.width},role:`group`,"aria-label":`人物关系图`,className:`people-graph-svg${d.compact?` compact`:``}`,children:[(0,x.jsx)(`title`,{children:`人物关系图`}),(0,x.jsx)(`desc`,{children:`点击节点打开人物的全部有效记忆,点击连线查看依据。悬停或聚焦连线查看摘要,也可使用画布下方的人物和连线列表。`}),e.edges.map(e=>{let n=f[e.source],r=f[e.target],i=`查看${m.get(e.source)}与${m.get(e.target)}的连线依据`,a=ii(e),s=p[a],l=e.links.some(e=>e.kind===`same_as`)?`possible`:e.links.length?`roleplay`:e.relationship_count?`relationship`:`cooccurrence`;return(0,x.jsxs)(`g`,{className:`graph-edge ${l}`,role:`button`,tabIndex:0,"aria-label":i,"aria-describedby":h===e?u:void 0,onMouseEnter:()=>o(a),onMouseLeave:()=>o(null),onFocus:()=>c(a),onBlur:()=>c(null),onClick:()=>t(e),onKeyDown:n=>{(n.key===`Enter`||n.key===` `)&&(n.preventDefault(),t(e))},children:[(0,x.jsxs)(`title`,{children:[i,`;`,e.memory_count,` 条共同涉及记忆`]}),(0,x.jsx)(`line`,{x1:n.x,y1:n.y,x2:r.x,y2:r.y,className:`graph-edge-hit`}),(0,x.jsx)(`line`,{x1:n.x,y1:n.y,x2:r.x,y2:r.y,strokeWidth:Math.min(7,1.5+Math.log2(1+e.memory_count))}),s&&(0,x.jsx)(`text`,{x:s.x,y:s.y,textAnchor:`middle`,dominantBaseline:`middle`,children:ai(e)})]},a)}),e.nodes.map(e=>(0,x.jsxs)(`a`,{href:di(e.id),"aria-label":`查看${e.name}的有效记忆`,className:`graph-node ${e.kind===`self`?`self`:``}`,children:[(0,x.jsxs)(`title`,{children:[e.name,`;别名:`,e.aliases.join(`、`)||`暂无`,`;`,e.account_count,` 个账号;`,e.memory_count,` 条有效记忆`]}),(0,x.jsx)(`circle`,{cx:f[e.id].x,cy:f[e.id].y,r:d.compact?14:Math.min(27,18+Math.log2(1+e.memory_count))}),(0,x.jsx)(`text`,{x:f[e.id].x,y:f[e.id].y+5,textAnchor:`middle`,className:`graph-node-mark`,children:e.kind===`self`?`我`:Array.from(e.name)[0]}),(0,x.jsx)(`rect`,{className:`graph-node-label-background`,x:f[e.id].x-(d.compact?44:78),y:f[e.id].y+(d.compact?20:30),width:d.compact?88:156,height:d.compact?33:38,rx:`4`}),(0,x.jsx)(`text`,{x:f[e.id].x,y:f[e.id].y+(d.compact?32:44),textAnchor:`middle`,children:ui(e.name,d.compact?6:10)}),(0,x.jsxs)(`text`,{x:f[e.id].x,y:f[e.id].y+(d.compact?48:63),textAnchor:`middle`,className:`graph-node-count`,children:[e.memory_count,` 条记忆`]})]},e.id))]})}),(0,x.jsx)(`div`,{className:`graph-preview`,children:h?(0,x.jsxs)(`div`,{id:u,role:`tooltip`,children:[(0,x.jsxs)(`strong`,{children:[m.get(h.source),` · `,m.get(h.target)]}),(0,x.jsxs)(`p`,{children:[ai(h),`(数字为共同涉及的有效记忆数)`]}),h.relationship_summaries.map(e=>(0,x.jsx)(`p`,{children:e.summary},e.id)),!h.relationship_summaries.length&&(0,x.jsx)(`p`,{children:`点击连线查看联系记录与支撑记忆。`})]}):(0,x.jsx)(`p`,{children:`悬停或用键盘聚焦连线可查看摘要;数字为共同涉及的有效记忆数。拥挤处省略标签,完整内容见下方列表。`})})]})}function hi({edge:e,names:t,entryId:n,onClose:r,openMemory:i}){let[a,o]=(0,l.useState)(0),s=y(`/people-graph/edge?${fi({source_id:e.source,target_id:e.target,entry_id:n,limit:`30`,offset:String(a)})}`);return(0,x.jsxs)(ee,{title:`连线依据:${t.get(e.source)} · ${t.get(e.target)}`,onClose:r,children:[(0,x.jsx)(`p`,{children:`这里只显示当前仍有效的记忆。身份/扮演联系来自独立联系记录;同来源记忆不等于确认身份,也不把角色经历归给扮演者。`}),s.error&&(0,x.jsxs)(A,{error:!0,children:[s.error,(0,x.jsx)(`button`,{onClick:s.refresh,children:`重新读取依据`})]}),s.loading&&(0,x.jsx)(`p`,{role:`status`,children:`正在读取连线依据…`}),s.data&&(0,x.jsxs)(x.Fragment,{children:[s.data.links.map(e=>(0,x.jsxs)(`article`,{className:`graph-link-record`,children:[(0,x.jsx)(`h3`,{children:e.kind===`same_as`?`可能同一人 · 未确认`:`扮演 · 虚构角色`}),(0,x.jsxs)(`p`,{children:[t.get(e.source),` `,e.kind===`roleplay`?`→`:`↔`,` `,t.get(e.target),` · 相信程度 `,e.belief]}),e.kind===`roleplay`&&(0,x.jsxs)(`p`,{children:[`场景:`,e.worlds.map(e=>e||`场景未记录`).join(`、`)]}),(0,x.jsxs)(`p`,{children:[`联系 #`,e.id,` · `,e.evidence_message_count,` 条来源消息`]}),(0,x.jsx)(`a`,{href:`#/people?id=${encodeURIComponent(e.source)}`,onClick:r,children:`到人物详情核对联系与消息`})]},e.id)),(0,x.jsxs)(`p`,{children:[`共 `,s.data.total,` 条支撑/同来源记忆`]}),!s.data.items.length&&(0,x.jsx)(j,{title:`没有可展示的有效支撑记忆`,children:`联系记录可能只有来源消息;遗忘、删除或清除的记忆不会在这里显示。`}),(0,x.jsx)(`div`,{className:`graph-evidence-list`,children:s.data.items.map(e=>(0,x.jsxs)(`article`,{children:[(0,x.jsxs)(`p`,{children:[(0,x.jsx)(M,{children:e.kind}),` #`,e.id,` · 修订`,` `,e.revision]}),(0,x.jsx)(`p`,{className:`memory-content`,children:e.content}),(0,x.jsxs)(`p`,{children:[e.cooccurrence&&(0,x.jsx)(M,{children:`共同涉及两人`}),` `,e.link_evidence&&(0,x.jsx)(M,{children:`同来源联系证据`})]}),(0,x.jsxs)(`button`,{onClick:()=>{r(),i(e.id)},children:[`查看记忆 #`,e.id]})]},e.id))}),(0,x.jsx)(T,{total:s.data.total,offset:a,change:o})]})]})}function gi({initialQuery:e=``,openMemory:t}){let n=new URLSearchParams(e),[r,i]=(0,l.useState)({entry_id:n.get(`entry_id`)||``,person_id:n.get(`person_id`)||``,hops:`1`,min_memories:`1`,node_limit:`60`,edge_limit:`100`}),[a,o]=(0,l.useState)(r),[s,c]=(0,l.useState)(null),u=y(`/catalog`),d=y(`/people-graph?${fi(a)}`),f=new Map(d.data?.nodes.map(e=>[e.id,e.name])||[]),p=e=>{c(null),o({...e}),fi(e)===fi(a)&&d.refresh()};return(0,x.jsxs)(x.Fragment,{children:[(0,x.jsxs)(`div`,{className:`page-heading`,children:[(0,x.jsxs)(`div`,{children:[(0,x.jsx)(`p`,{className:`eyebrow`,children:`从已有经历看人物联系`}),(0,x.jsx)(`h1`,{children:`人物关系图`}),(0,x.jsx)(`p`,{children:`点击人物查看记忆,点击连线追溯依据。`})]}),(0,x.jsx)(`a`,{href:`#/people`,className:`button`,children:`返回人物列表`})]}),(0,x.jsxs)(`form`,{className:`panel graph-filters`,onSubmit:e=>{e.preventDefault(),p(r)},children:[(0,x.jsxs)(`label`,{children:[`来源入口`,(0,x.jsxs)(`select`,{value:r.entry_id,onChange:e=>i({...r,entry_id:e.target.value}),children:[(0,x.jsx)(`option`,{value:``,children:`全部入口`}),u.data?.entries.map(e=>(0,x.jsx)(`option`,{value:e.id,children:e.name},e.id))]})]}),(0,x.jsxs)(`label`,{children:[`聚焦人物`,(0,x.jsxs)(`select`,{value:r.person_id,onChange:e=>i({...r,person_id:e.target.value}),children:[(0,x.jsx)(`option`,{value:``,children:`所有人物`}),u.data?.people.filter(e=>e.id!==`scene`).map(e=>(0,x.jsxs)(`option`,{value:e.id,children:[e.name,e.id===`self`?`(角色)`:``,` · `,e.id]},e.id))]})]}),(0,x.jsxs)(`label`,{children:[`范围`,(0,x.jsxs)(`select`,{value:r.hops,disabled:!r.person_id,onChange:e=>i({...r,hops:e.target.value}),children:[(0,x.jsx)(`option`,{value:`1`,children:`一跳联系`}),(0,x.jsx)(`option`,{value:`2`,children:`两跳联系`})]})]}),(0,x.jsxs)(`label`,{children:[`共现最少记忆数`,(0,x.jsx)(`input`,{type:`number`,min:`1`,max:`1000000`,required:!0,value:r.min_memories,onChange:e=>i({...r,min_memories:e.target.value})})]}),(0,x.jsxs)(`label`,{children:[`节点上限`,(0,x.jsx)(`input`,{type:`number`,min:`2`,max:`100`,required:!0,value:r.node_limit,onChange:e=>i({...r,node_limit:e.target.value})})]}),(0,x.jsxs)(`label`,{children:[`连线上限`,(0,x.jsx)(`input`,{type:`number`,min:`1`,max:`200`,required:!0,value:r.edge_limit,onChange:e=>i({...r,edge_limit:e.target.value})})]}),(0,x.jsx)(`button`,{className:`primary`,disabled:d.loading,children:`生成关系图`})]}),u.error&&(0,x.jsxs)(A,{error:!0,children:[`筛选目录:`,u.error,(0,x.jsx)(`button`,{onClick:u.refresh,children:`重新读取目录`})]}),(0,x.jsxs)(`details`,{className:`panel graph-explanation`,children:[(0,x.jsx)(`summary`,{children:`数据口径与图例`}),(0,x.jsx)(`p`,{children:`只统计 active 且未彻底清除的记忆。共现按同一记忆的 about 涉及人两两计数,每条只算一次;共现不等于关系事实。“关系”类记忆另外显示正文摘要。`}),(0,x.jsx)(`p`,{children:`入口筛选只认至少一条直接来源消息在该入口的记忆,不沿派生依据扩大范围。身份/扮演联系按其来源消息筛选,可没有支撑记忆;共现阈值不隐藏这些独立联系。`}),(0,x.jsx)(`p`,{children:`节点记忆数包括作为涉及人或说话人的有效记忆,别名与账号数为当前主体资料。self 显示为角色节点,scene 不作为人物;已合并主体只显示保留人物。点击人物打开其全部入口的有效记忆列表。`}),(0,x.jsx)(`p`,{children:`线条越粗,共同涉及记忆越多;紫色为关系记忆,虚线为可能同一人,绿色为扮演。位置和距离只用于排布,没有关系强弱含义。图中读数不调用模型,不产生召回或使用记录。`})]}),d.error&&(0,x.jsxs)(A,{error:!0,children:[d.error,(0,x.jsx)(`button`,{onClick:d.refresh,children:`重新读取关系图`})]}),d.loading&&(0,x.jsx)(`p`,{role:`status`,children:`正在生成只读关系图…`}),d.data&&(0,x.jsxs)(x.Fragment,{children:[(0,x.jsxs)(`p`,{"aria-live":`polite`,children:[`当前显示 `,d.data.nodes.length,` 个节点、`,d.data.edges.length,` `,`条连线。`]}),d.data.truncated&&(0,x.jsxs)(A,{children:[`筛选后共有 `,d.data.total_nodes,` 个节点、`,d.data.total_edges,` `,`条连线,超过显示上限。按共同涉及记忆数从高到低保留,关系记忆数和主体标识用于同权重排序;聚焦人物优先保留。可缩小入口或聚焦范围后重新查看。`]}),d.data.nodes.length?(0,x.jsxs)(x.Fragment,{children:[(0,x.jsxs)(`section`,{className:`panel graph-panel`,"aria-label":`关系图与图例`,children:[(0,x.jsxs)(`div`,{className:`graph-legend`,children:[(0,x.jsx)(`span`,{children:`实线:共现`}),(0,x.jsx)(`span`,{children:`紫色:关系记忆`}),(0,x.jsx)(`span`,{children:`虚线:可能同一人`}),(0,x.jsx)(`span`,{children:`绿色:扮演`})]}),(0,x.jsx)(mi,{data:d.data,onEdge:c}),(0,x.jsx)(`p`,{className:`muted`,children:`窄屏可横向滚动画布。下方列表提供相同的跳转和证据操作。`})]}),(0,x.jsxs)(`section`,{"aria-label":`图中人物`,children:[(0,x.jsx)(`h2`,{children:`图中人物`}),(0,x.jsx)(`div`,{className:`graph-node-list`,children:d.data.nodes.map(e=>(0,x.jsxs)(`article`,{className:`panel`,children:[(0,x.jsxs)(`h3`,{children:[(0,x.jsx)(`a`,{href:di(e.id),"aria-label":`查看${e.name}的有效记忆`,children:e.name}),` `,e.kind===`self`&&(0,x.jsx)(M,{children:`角色`})]}),(0,x.jsx)(`small`,{className:`muted`,children:e.id}),(0,x.jsxs)(`p`,{children:[`别名:`,e.aliases.join(`、`)||`暂无`]}),(0,x.jsxs)(`p`,{children:[e.account_count,` 个账号 · `,e.memory_count,` `,`条有效记忆`]}),(0,x.jsx)(`button`,{className:`text-button`,"aria-label":`聚焦${e.name}`,onClick:()=>{let t={...r,person_id:e.id};i(t),p(t)},children:`以此人为中心`})]},e.id))})]}),(0,x.jsxs)(`section`,{"aria-label":`图中连线`,children:[(0,x.jsx)(`h2`,{children:`图中连线`}),!d.data.edges.length&&(0,x.jsx)(`p`,{className:`muted`,children:`当前阈值和范围没有连线。`}),(0,x.jsx)(`div`,{className:`graph-edge-list`,children:d.data.edges.map(e=>(0,x.jsxs)(`article`,{className:`panel`,children:[(0,x.jsxs)(`h3`,{children:[f.get(e.source),` · `,f.get(e.target)]}),(0,x.jsx)(pi,{edge:e}),(0,x.jsxs)(`p`,{children:[e.memory_count,` 条共同涉及记忆`]}),e.relationship_summaries.map(e=>(0,x.jsxs)(`p`,{children:[`关系记忆 #`,e.id,`:`,e.summary]},e.id)),e.relationship_count>e.relationship_summaries.length&&(0,x.jsx)(`small`,{children:`更多关系正文见连线依据。`}),(0,x.jsx)(`button`,{onClick:()=>c(e),"aria-label":`查看${f.get(e.source)}与${f.get(e.target)}的连线依据`,children:`查看连线依据`})]},JSON.stringify([e.source,e.target])))})]})]}):(0,x.jsx)(j,{title:`当前范围没有人物关系数据`,children:`可以改选入口或人物;遗忘、删除和清除的记忆不参与关系图。`})]}),s&&(0,x.jsx)(hi,{edge:s,names:f,entryId:a.entry_id,onClose:()=>c(null),openMemory:t},JSON.stringify([s.source,s.target,a.entry_id]))]})}function _i({initialVersion:e,initialComparison:t,currentID:n,catalog:r,openMemory:i,onAction:a,locked:o}){let[s,c]=(0,l.useState)(e),[u,d]=(0,l.useState)({status:``,source:``}),[f,p]=(0,l.useState)(``),[m,h]=(0,l.useState)(0),g=y(`/persona/versions?${f}limit=30&offset=${m}`,5e3),[_,v]=(0,l.useState)(t?.before.toString()||``),[b,S]=(0,l.useState)(t?.after.toString()||n?.toString()||``),[C,w]=(0,l.useState)(``),[E,D]=(0,l.useState)(t||null),[O,k]=(0,l.useState)(0);return(0,x.jsxs)(x.Fragment,{children:[(0,x.jsxs)(`section`,{className:`panel persona-history`,children:[(0,x.jsx)(`h2`,{children:`版本历史`}),(0,x.jsx)(`p`,{children:`可选择任意两个版本逐句对比。只有已经发布过的版本可以回滚;回滚会新建版本。`}),(0,x.jsxs)(`form`,{className:`persona-filters`,onSubmit:e=>{e.preventDefault();let t=new URLSearchParams;u.status&&t.set(`status`,u.status),u.source&&t.set(`source`,u.source),p(t.size?`${t}&`:``),h(0),g.refresh()},children:[(0,x.jsxs)(`label`,{children:[`版本状态`,(0,x.jsxs)(`select`,{value:u.status,onChange:e=>d({...u,status:e.target.value}),children:[(0,x.jsx)(`option`,{value:``,children:`全部状态`}),Object.entries(ne).map(([e,t])=>(0,x.jsx)(`option`,{value:e,children:t},e))]})]}),(0,x.jsxs)(`label`,{children:[`版本来源`,(0,x.jsxs)(`select`,{value:u.source,onChange:e=>d({...u,source:e.target.value}),children:[(0,x.jsx)(`option`,{value:``,children:`全部来源`}),Object.entries(L).map(([e,t])=>(0,x.jsx)(`option`,{value:e,children:t},e))]})]}),(0,x.jsx)(`button`,{children:`筛选版本`})]}),g.error&&(0,x.jsxs)(A,{error:!0,children:[g.error,(0,x.jsx)(`button`,{onClick:g.refresh,children:`重试版本列表`})]}),g.loading&&(0,x.jsx)(`p`,{children:`正在读取版本…`}),g.data?.items.map(e=>(0,x.jsxs)(`article`,{className:`persona-history-row`,children:[(0,x.jsxs)(`div`,{children:[(0,x.jsxs)(`strong`,{children:[`v`,e.id]}),` `,(0,x.jsx)(M,{children:ne[e.status]}),` ·`,` `,L[e.source],` · 变化`,` `,e.change_degree?R[e.change_degree]:`未记录`,(0,x.jsx)(`p`,{className:`muted`,children:(0,x.jsx)(N,{value:e.generated_at})}),(0,x.jsx)(`p`,{className:`persona-content`,children:e.content})]}),(0,x.jsxs)(`div`,{className:`persona-actions`,children:[(0,x.jsxs)(`button`,{onClick:()=>c(e.id),children:[`查看版本 v`,e.id]}),(0,x.jsxs)(`button`,{onClick:()=>v(String(e.id)),children:[`v`,e.id,` 作为对比前版`]}),(0,x.jsxs)(`button`,{onClick:()=>S(String(e.id)),children:[`v`,e.id,` 作为对比后版`]})]})]},e.id)),g.data?.total===0&&(0,x.jsx)(j,{title:`没有符合筛选的版本`}),g.data&&(0,x.jsx)(T,{total:g.data.total,offset:m,change:h})]}),(0,x.jsxs)(`section`,{className:`panel persona-comparison`,children:[(0,x.jsx)(`h2`,{children:`对比任意版本`}),(0,x.jsx)(`p`,{children:`从列表选择,或输入版本 ID;可以跨页选择,也可以查看被拒绝的候选。`}),(0,x.jsxs)(`form`,{noValidate:!0,className:`persona-filters`,onSubmit:e=>{if(e.preventDefault(),!/^\d+$/.test(_)||!/^\d+$/.test(b)||!Number.isSafeInteger(Number(_))||!Number.isSafeInteger(Number(b))||Number(_)<1||Number(b)<1){w(`请输入有效的正整数版本 ID`);return}w(``),D({before:Number(_),after:Number(b)}),k(e=>e+1)},children:[(0,x.jsxs)(`label`,{children:[`对比前版本 ID`,(0,x.jsx)(`input`,{type:`number`,min:1,step:1,value:_,onChange:e=>v(e.target.value)})]}),(0,x.jsxs)(`label`,{children:[`对比后版本 ID`,(0,x.jsx)(`input`,{type:`number`,min:1,step:1,value:b,onChange:e=>S(e.target.value)})]}),(0,x.jsx)(`button`,{children:`对比版本`})]}),C&&(0,x.jsx)(A,{error:!0,children:C}),E&&(0,x.jsx)(Ut,{...E},O)]}),s&&(0,x.jsx)(Vt,{id:s,title:`版本详情`,catalog:r,openMemory:i,onAction:a,locked:o,currentID:n,history:!0},s)]})}function vi({openMemory:e}){let[t,n]=(0,l.useState)(0),r=y(`/persona/self-memories?limit=30&offset=${t}`);return(0,x.jsxs)(`section`,{className:`panel persona-memories`,children:[(0,x.jsx)(`h2`,{children:`自我记忆`}),(0,x.jsx)(`p`,{children:`有效的自我设定与经历是 persona 的材料;打开或翻页不会触发召回。`}),r.error&&(0,x.jsxs)(A,{error:!0,children:[r.error,(0,x.jsx)(`button`,{onClick:r.refresh,children:`重试自我记忆`})]}),r.loading&&(0,x.jsx)(`p`,{children:`正在读取自我记忆…`}),r.data?.items.map(t=>(0,x.jsxs)(`article`,{className:`persona-history-row`,children:[(0,x.jsx)(`p`,{className:`persona-content`,children:t.content}),(0,x.jsxs)(`p`,{className:`muted`,children:[`#`,t.id,` · 修订 `,t.revision,` · `,t.stance,` ·`,` `,E(t.lifecycle),` · `,(0,x.jsx)(N,{value:t.created_at})]}),(0,x.jsxs)(`button`,{className:`text-button`,onClick:()=>e(t.id),children:[`查看自我记忆 #`,t.id]})]},t.id)),r.data?.total===0&&(0,x.jsx)(j,{title:`暂无有效自我记忆`,children:`在持续对话中积累经历,或在设置中完善初始背景。`}),r.data&&(0,x.jsx)(T,{total:r.data.total,offset:t,change:n})]})}function yi(){let[e,t]=(0,l.useState)(0),n=y(`/persona/attempts?limit=30&offset=${e}`,2e3);return(0,x.jsxs)(`section`,{className:`panel persona-attempts`,children:[(0,x.jsx)(`h2`,{children:`生成记录`}),(0,x.jsx)(`p`,{children:`包括未生成版本的跳过、冲突与失败;生成被拒绝不改变当前 persona。`}),n.error&&(0,x.jsxs)(A,{error:!0,children:[n.error,(0,x.jsx)(`button`,{onClick:n.refresh,children:`重试生成记录`})]}),n.loading&&(0,x.jsx)(`p`,{children:`正在读取生成记录…`}),n.data?.items.map(e=>(0,x.jsxs)(`article`,{className:`persona-history-row`,children:[(0,x.jsxs)(`h3`,{children:[`任务 #`,e.id,` · `,se(e.state,e.stage)]}),(0,x.jsxs)(`p`,{children:[L[e.source]||e.source,` · 基于 v`,e.base_version_id]}),(0,x.jsxs)(`p`,{className:`muted`,children:[`接受:`,(0,x.jsx)(N,{value:e.created_at}),` · 结束:`,(0,x.jsx)(N,{value:e.finished_at})]}),e.reason&&(0,x.jsx)(`p`,{children:ae(e.reason)}),e.version_id&&(0,x.jsxs)(`a`,{href:`#/persona?tab=history&version=${e.version_id}`,children:[`查看版本 v`,e.version_id]}),(0,x.jsx)(`p`,{children:(0,x.jsx)(`a`,{href:`#/operations?object_type=persona_attempt&object_id=${e.id}`,children:`查看任务操作记录`})})]},e.id)),n.data?.total===0&&(0,x.jsx)(j,{title:`暂无生成任务`}),n.data&&(0,x.jsx)(T,{total:n.data.total,offset:e,change:t})]})}function bi({initial:e,path:t,onFinish:n}){let[r,i]=(0,l.useState)(e),[a,o]=(0,l.useState)(``),[s,c]=(0,l.useState)(0),u=(0,l.useRef)(n);u.current=n;let d=oe(e.state);(0,l.useEffect)(()=>{if(!d){i(e),o(``);return}let n=new AbortController,r=!0,a,s=async()=>{let c=!0;try{let a=await g(t||`/persona/attempts/${e.id}`,{signal:n.signal});if(!r)return;i(a),o(``),oe(a.state)||(c=!1,u.current(a))}catch(e){r&&!n.signal.aborted&&o(v(e))}r&&c&&(a=setTimeout(s,2e3))};return s(),()=>{r=!1,n.abort(),clearTimeout(a)}},[e.id,d,t,s]);let f=y(r.state===`rejected`&&r.version_id?`/persona/versions/${r.version_id}`:null);return(0,x.jsxs)(`section`,{className:`panel persona-task`,"aria-label":`最近生成任务`,children:[(0,x.jsxs)(`h2`,{children:[`生成任务 #`,r.id]}),(0,x.jsxs)(`p`,{role:`status`,children:[se(r.state,r.stage),r.state===`current`&&r.version_id?` · v${r.version_id}`:``]}),oe(r.state)&&(0,x.jsx)(`p`,{children:`请求已接受,尚未发布。页面会持续查询生成与检查结果;离开后任务仍在服务端运行。`}),r.reason&&(0,x.jsx)(A,{error:r.state===`failed`||r.state===`conflict`,children:ae(r.reason)}),(0,x.jsxs)(`p`,{className:`muted`,children:[`基于 v`,r.base_version_id,` · 接受时间:`,(0,x.jsx)(N,{value:r.created_at}),r.finished_at&&(0,x.jsxs)(x.Fragment,{children:[` `,`· 结束时间:`,(0,x.jsx)(N,{value:r.finished_at})]})]}),a&&(0,x.jsxs)(A,{error:!0,children:[a,`。任务结果尚未获知,将继续查询。`,(0,x.jsx)(`button`,{onClick:()=>c(e=>e+1),children:`立即查询任务`})]}),f.error&&(0,x.jsxs)(A,{error:!0,children:[f.error,(0,x.jsx)(`button`,{onClick:f.refresh,children:`重试拒绝原因`})]}),f.data&&(0,x.jsxs)(`div`,{children:[(0,x.jsx)(`h3`,{children:`拒绝原因`}),f.data.rejection_reasons.length?(0,x.jsx)(`ul`,{children:f.data.rejection_reasons.map((e,t)=>(0,x.jsx)(`li`,{children:ae(e)},t))}):(0,x.jsx)(`p`,{children:`未记录拒绝原因,请查看版本检查结果。`})]}),r.version_id&&(0,x.jsxs)(`a`,{href:`#/persona?tab=history&version=${r.version_id}`,children:[`查看结果版本 v`,r.version_id]}),(0,x.jsx)(`p`,{children:(0,x.jsx)(`a`,{href:`#/operations?object_type=persona_attempt&object_id=${r.id}`,children:`查看生成操作记录`})})]})}var xi={edit:`编辑并发布 persona`,confirm:`确认发布候选`,reject:`拒绝候选`,rollback:`回滚 persona`},Si={edit:`发布编辑`,confirm:`确认发布`,reject:`确认拒绝`,rollback:`确认回滚`},Ci={current:`当前与候选`,history:`版本历史`,memories:`自我记忆`,attempts:`生成记录`};function wi({openMemory:e,initialQuery:t=``}){let n=new URLSearchParams(t),r=Number(n.get(`version`)),[i,a]=(0,l.useState)(n.get(`focus`)===`pending`),o=(0,l.useRef)(null),[s,c]=(0,l.useState)(Object.hasOwn(Ci,n.get(`tab`)||``)?n.get(`tab`):`current`),u=y(`/persona`,2e3),d=y(`/catalog`),[p,h]=(0,l.useState)(null),[b,S]=(0,l.useState)(``),[C,w]=(0,l.useState)(``),[T,E]=(0,l.useState)(``),[D,O]=(0,l.useState)(``),[k,N]=(0,l.useState)(!1),[P,F]=(0,l.useState)(!1),[I,te]=(0,l.useState)(0),[L,ne]=(0,l.useState)(null),R=u.data,re=R?.current,ie=R?.pending;(0,l.useEffect)(()=>{i&&s===`current`&&ie&&o.current&&(o.current.focus(),o.current.scrollIntoView?.({block:`start`}),a(!1))},[i,s,ie]);let se=R?.latest_attempt,ce=se&&(!L||se.id>L.attempt.id||se.id===L.attempt.id&&oe(L.attempt.state)&&!oe(se.state))?{attempt:se,path:L?.attempt.id===se.id?L.path:void 0}:L,le=k||P||!!u.error||u.loading;function ue(e,t){re&&!le&&(h({kind:e,version:t,expectedVersion:re.id}),S(t.content),w(``),E(``),O(``))}function de(){h(null),F(!1),E(``),O(``),u.refresh(),te(e=>e+1)}function z(e){E(v(e)),e instanceof f&&e.code===`persona_conflict`&&F(!0)}async function B(){if(p&&!le){if(p.kind===`edit`&&(!b.trim()||[...b].length>800)){E(`persona 正文须为 1—800 字,不能只含空白`);return}if(p.kind===`reject`&&[...C.trim()].length>1e3){E(`拒绝原因不能超过 1000 字`);return}N(!0),E(``);try{let e={expected_version:p.expectedVersion,...p.kind===`edit`?{content:b}:p.kind===`reject`&&C.trim()?{reason:C.trim()}:{}},t=await g(p.kind===`edit`?`/persona`:`/persona/versions/${p.version.id}/${p.kind}`,_(p.kind===`edit`?`PUT`:`POST`,e));O(p.kind===`reject`?`已拒绝候选 v${t.id},当前版本仍保留。`:`已发布 persona v${t.id}${p.kind===`rollback`?`,回滚自 v${p.version.id}`:``}。`),h(null),c(`current`),u.refresh(),te(e=>e+1)}catch(e){z(e)}finally{N(!1)}}}async function V(){if(!(!re||le||oe(ce?.attempt.state))){N(!0),E(``),O(``);try{let e=await m(`/persona/regenerate`,_(`POST`,{expected_version:re.id}));if(e.status!==202||!e.data.accepted)throw new f(`invalid_response`,`生成请求返回了意外结果,请刷新查看任务记录;不要重复提交。`,e.status);let t=e.data.attempt,n=e.headers.get(`Location`),r=n===`/admin/api/persona/attempts/${t.id}`?n.slice(10):`/persona/attempts/${t.id}`;ne({attempt:t,path:r}),u.refresh()}catch(e){z(e),u.refresh()}finally{N(!1)}}}let fe=(0,x.jsx)(`button`,{disabled:k,onClick:de,children:`重新加载当前版本`});return(0,x.jsxs)(`div`,{className:`persona-page`,children:[(0,x.jsxs)(`div`,{className:`page-heading`,children:[(0,x.jsxs)(`div`,{children:[(0,x.jsx)(`span`,{className:`eyebrow`,children:`自我与未来`}),(0,x.jsx)(`h1`,{children:`persona 与自我`}),(0,x.jsx)(`p`,{children:`从持续经历中认识自己,逐句查看变化的依据。`})]}),(0,x.jsxs)(`div`,{className:`persona-actions`,children:[(0,x.jsx)(`button`,{disabled:k,onClick:u.refresh,children:`刷新 persona`}),re&&(0,x.jsxs)(x.Fragment,{children:[(0,x.jsx)(`button`,{disabled:le,onClick:()=>ue(`edit`,re),children:`编辑并发布`}),(0,x.jsx)(`button`,{className:`primary`,disabled:le||oe(ce?.attempt.state),onClick:()=>void V(),children:`重新生成`})]})]})]}),u.error&&(0,x.jsxs)(A,{error:!0,children:[u.error,`。数据可能尚未更新,写操作暂不可用。`]}),d.error&&(0,x.jsxs)(A,{error:!0,children:[`入口类型暂不可用。`,d.error,(0,x.jsx)(`button`,{onClick:d.refresh,children:`重试入口信息`})]}),!p&&T&&(0,x.jsxs)(A,{error:!0,children:[T,P&&(0,x.jsxs)(x.Fragment,{children:[(0,x.jsx)(`p`,{children:`请重新加载后核对当前版本,再决定是否操作。`}),fe]})]}),D&&(0,x.jsx)(A,{children:D}),R?.needs_update&&(0,x.jsxs)(`section`,{className:`notice persona-stale`,"aria-label":`失效依据`,children:[(0,x.jsxs)(`h2`,{children:[(0,x.jsx)(M,{tone:`warning`,children:`待更新`}),` `,R.stale_basis_count,` `,`条依据失效`]}),(0,x.jsx)(`p`,{children:`当前 persona 仍在使用。重新生成会重新提炼与检查;失效依据不等于已有待确认候选。`}),(0,x.jsx)(`ul`,{children:R.stale_basis.map((t,n)=>(0,x.jsxs)(`li`,{children:[`记忆 #`,t.memory_id,` · 记录修订 `,t.expected_revision,` ·`,` `,t.current_revision==null?`当前不可用`:`当前修订 ${t.current_revision}`,`:`,ae(t.reason),t.memory&&(0,x.jsxs)(`button`,{className:`text-button`,onClick:()=>e(t.memory_id),children:[`查看失效依据 #`,t.memory_id]})]},`${t.memory_id}-${n}`))})]}),ie&&(0,x.jsxs)(A,{children:[(0,x.jsx)(M,{tone:`warning`,children:`待确认`}),` 候选 v`,ie.id,` `,`等待决定,当前版本仍在使用。`,(0,x.jsx)(`button`,{className:`text-button`,onClick:()=>{c(`current`),a(!0)},children:`查看候选与差异`})]}),R&&!ie&&n.get(`focus`)===`pending`&&(0,x.jsx)(A,{children:`当前没有待确认的 persona 候选。`}),ce&&(0,x.jsx)(bi,{initial:ce.attempt,path:ce.path,onFinish:e=>{ne({attempt:e,path:ce.path}),u.refresh(),te(e=>e+1)}},ce.attempt.id),(0,x.jsx)(U,{}),(0,x.jsx)(`div`,{className:`tabs persona-tabs`,role:`tablist`,"aria-label":`persona 内容`,children:Object.entries(Ci).map(([e,t])=>(0,x.jsx)(`button`,{id:`persona-tab-${e}`,role:`tab`,"aria-selected":s===e,tabIndex:s===e?0:-1,onKeyDown:t=>{let n=Object.keys(Ci),r=n.indexOf(e),i=t.key===`ArrowRight`?(r+1)%n.length:t.key===`ArrowLeft`?(r+n.length-1)%n.length:t.key===`Home`?0:t.key===`End`?n.length-1:null;i!==null&&(t.preventDefault(),c(n[i]),document.getElementById(`persona-tab-${n[i]}`)?.focus())},"aria-controls":`persona-tab-panel`,className:s===e?`active`:``,onClick:()=>c(e),children:t},e))}),(0,x.jsxs)(`div`,{role:`tabpanel`,id:`persona-tab-panel`,"aria-labelledby":`persona-tab-${s}`,children:[s===`current`&&(0,x.jsxs)(x.Fragment,{children:[!R&&!u.error&&(0,x.jsx)(`p`,{children:`正在读取 persona…`}),R&&!re&&(0,x.jsx)(j,{title:`暂无 persona 版本`,children:`请先在设置中完成角色初始设定。`}),re&&(0,x.jsx)(Vt,{id:re.id,title:`当前 persona`,catalog:d.data,openMemory:e},`current-${re.id}-${I}`),ie&&(0,x.jsxs)(`div`,{ref:o,role:`region`,"aria-label":`待确认候选与差异`,tabIndex:-1,children:[(0,x.jsx)(Vt,{id:ie.id,title:`待确认候选`,catalog:d.data,openMemory:e,onAction:ue,currentID:re?.id,locked:le},`pending-${ie.id}-${I}`),re&&(0,x.jsx)(`div`,{className:`panel`,children:(0,x.jsx)(Ut,{before:re.id,after:ie.id},`${re.id}-${ie.id}-${I}`)})]})]}),s===`history`&&(0,x.jsx)(_i,{initialComparison:(()=>{let e=Number(n.get(`before`)),t=Number(n.get(`after`));return Number.isSafeInteger(e)&&e>0&&Number.isSafeInteger(t)&&t>0?{before:e,after:t}:void 0})(),initialVersion:Number.isSafeInteger(r)&&r>0?r:void 0,currentID:re?.id,catalog:d.data,openMemory:e,onAction:ue,locked:le}),s===`memories`&&(0,x.jsx)(vi,{openMemory:e}),s===`attempts`&&(0,x.jsx)(yi,{})]}),p&&(0,x.jsx)(ee,{title:xi[p.kind],closeDisabled:k,onClose:()=>h(null),children:(0,x.jsxs)(`form`,{noValidate:!0,className:`persona-action-form`,onSubmit:e=>{e.preventDefault(),B()},children:[p.kind===`edit`?(0,x.jsxs)(x.Fragment,{children:[(0,x.jsxs)(`p`,{children:[`发布会替换当前 v`,p.expectedVersion,` `,`并新建版本;新增或改动的句子会标为手写。待确认候选会被替代。`]}),(0,x.jsx)(`p`,{children:`之后的自动候选若删去手写内容,按“大变化”处理:默认方式需人工确认;“全部自动”模式下,检查通过仍会直接发布。`}),(0,x.jsxs)(`label`,{children:[`persona 正文`,(0,x.jsx)(`textarea`,{rows:9,disabled:k,value:b,onChange:e=>S(e.target.value)})]}),(0,x.jsxs)(`small`,{children:[[...b].length,` / 800 字`]})]}):p.kind===`confirm`?(0,x.jsxs)(`p`,{children:[`发布候选 v`,p.version.id,` 会替换当前 v`,p.expectedVersion,`。旧版本保留在历史中,可在之后回滚;服务会再次核对版本和依据。`]}):p.kind===`reject`?(0,x.jsxs)(x.Fragment,{children:[(0,x.jsxs)(`p`,{children:[`拒绝候选 v`,p.version.id,`,保留当前 v`,p.expectedVersion,`。拒绝记录与原因会保存;采用新候选需要再次生成。`]}),(0,x.jsxs)(`label`,{children:[`拒绝原因(可选)`,(0,x.jsx)(`textarea`,{rows:3,maxLength:1e3,disabled:k,value:C,onChange:e=>w(e.target.value)})]})]}):(0,x.jsxs)(`p`,{children:[`使用 v`,p.version.id,` 的正文与依据新建一个版本,替换当前 v`,p.expectedVersion,`;历史版本保持不变。待确认候选会被替代,旧依据失效时新版本仍会标为待更新。`]}),T&&(0,x.jsxs)(A,{error:!0,children:[T,P&&(0,x.jsxs)(x.Fragment,{children:[(0,x.jsx)(`p`,{children:`重新加载会放弃本次未保存草稿。可先复制草稿,再核对最新版本。`}),(0,x.jsx)(`button`,{type:`button`,onClick:de,children:`重新加载当前版本`})]})]}),(0,x.jsxs)(`div`,{className:`persona-actions`,children:[(0,x.jsx)(`button`,{type:`button`,disabled:k,onClick:()=>h(null),children:`取消`}),(0,x.jsx)(`button`,{className:`primary`,disabled:le,children:k?`正在提交…`:Si[p.kind]})]})]})})]})}var Ti=[{id:`trial`,name:`试用对话`,icon:`chat`},{id:`memories`,name:`记忆`,icon:`memory`},{id:`people`,name:`人物`,icon:`people`},{id:`learning`,name:`入口与学习`,icon:`learning`},{id:`persona`,name:`persona 与自我`,icon:`people`},{id:`state`,name:`状态与目标`,icon:`state`},{id:`status`,name:`运行状态`,icon:`status`},{id:`operations`,name:`操作记录`,icon:`memory`},{id:`connections`,name:`接入与数据`,icon:`learning`},{id:`settings`,name:`设置`,icon:`settings`}];function Ei({name:e}){return(0,x.jsx)(`svg`,{viewBox:`0 0 24 24`,fill:`none`,stroke:`currentColor`,strokeWidth:`1.7`,strokeLinecap:`round`,strokeLinejoin:`round`,"aria-hidden":!0,children:e===`chat`?(0,x.jsx)(`path`,{d:`M20 11.5a8 8 0 0 1-8 8H5l-3 2 1.5-5A8 8 0 1 1 20 11.5ZM7 10h8M7 14h5`}):e===`people`?(0,x.jsxs)(x.Fragment,{children:[(0,x.jsx)(`circle`,{cx:`9`,cy:`7`,r:`3`}),(0,x.jsx)(`path`,{d:`M3 21v-3a6 6 0 0 1 12 0v3M16 4a3 3 0 0 1 0 6M18 14a5 5 0 0 1 3 4v3`})]}):e===`learning`?(0,x.jsx)(x.Fragment,{children:(0,x.jsx)(`path`,{d:`M4 4h16v5H4zM4 14h7v6H4zM16 14h4v6h-4M8 9v5M18 9v5`})}):e===`state`?(0,x.jsxs)(x.Fragment,{children:[(0,x.jsx)(`circle`,{cx:`12`,cy:`12`,r:`9`}),(0,x.jsx)(`path`,{d:`M12 7v5l3 2`})]}):e===`memory`?(0,x.jsxs)(x.Fragment,{children:[(0,x.jsx)(`rect`,{x:`5`,y:`3`,width:`14`,height:`18`,rx:`3`}),(0,x.jsx)(`path`,{d:`M9 8h6M9 12h6M9 16h3`})]}):(0,x.jsx)(x.Fragment,{children:(0,x.jsx)(`path`,{d:`M3 13h4l3-7 4 13 3-7h4`})})})}function Di(){return(0,x.jsx)(Ie,{children:e=>(0,x.jsx)(Oi,{refreshSession:e})})}function Oi({refreshSession:e}){let[t,n]=(0,l.useState)(location.hash.slice(2)||`trial`),[r,i]=(0,l.useState)(null),[a,o]=(0,l.useState)(0),s=y(`/status`,2e3),c=y(`/people?pending_only=true&limit=1&offset=0`,5e3),u=t.split(`?`)[0],d=()=>{o(e=>e+1),s.refresh()};(0,l.useEffect)(()=>{let e=()=>{n(location.hash.slice(2)||`trial`),i(null)};return addEventListener(`hashchange`,e),()=>removeEventListener(`hashchange`,e)},[]);let f=(0,l.useCallback)(e=>i(e),[]),p=(0,l.useCallback)(()=>i(null),[]),m=s.data?.model_health.chat;return(0,x.jsxs)(`div`,{className:`app`,children:[(0,x.jsx)(`a`,{className:`skip-link`,href:`#main`,onClick:e=>{e.preventDefault(),document.getElementById(`main`)?.focus()},children:`跳到主内容`}),(0,x.jsxs)(`header`,{className:`app-header`,children:[(0,x.jsxs)(`a`,{href:`#/trial`,className:`brand`,"aria-label":`Iris 首页`,children:[(0,x.jsx)(`span`,{className:`brand-symbol`,"aria-hidden":!0,children:`✳`}),(0,x.jsxs)(`span`,{children:[`Iris`,(0,x.jsx)(`small`,{children:`相处与记忆`})]})]}),(0,x.jsx)(`nav`,{"aria-label":`主导航`,children:Ti.map(e=>(0,x.jsxs)(`a`,{href:`#/${e.id}`,"aria-current":u===e.id||u===`people-graph`&&e.id===`people`?`page`:void 0,children:[(0,x.jsx)(Ei,{name:e.icon}),e.name,e.id===`people`&&!!c.data?.total&&(0,x.jsx)(`span`,{className:`nav-dot`,"aria-label":`有待确认的人物联系`})]},e.id))}),(0,x.jsx)(`button`,{className:`text-button logout-button`,onClick:async()=>{await g(`/logout`,_(`POST`,{})),e()},children:`退出登录`}),(0,x.jsxs)(`span`,{className:`local-badge`,children:[(0,x.jsx)(`span`,{"aria-hidden":!0,children:`◉`}),` 仅本机`]})]}),(0,x.jsx)(`main`,{id:`main`,tabIndex:-1,children:(0,x.jsxs)(`div`,{className:`content-wrap`,children:[!!c.data?.total&&u===`trial`&&(0,x.jsxs)(A,{children:[c.data.total,` 位人物有待确认的“可能是同一人”联系。`,(0,x.jsx)(`a`,{href:`#/people?pending_only=true`,children:`查看待确认联系`})]}),c.error&&u===`trial`&&(0,x.jsxs)(A,{error:!0,children:[`人物联系提示暂时无法更新。`,(0,x.jsx)(`button`,{onClick:c.refresh,children:`重试人物提示`})]}),s.error&&(0,x.jsxs)(A,{error:!0,children:[s.error,`。以下数据可能尚未更新。`,(0,x.jsx)(`button`,{className:`text-button`,onClick:s.refresh,children:`重新连接`})]}),m&&m.state!==`normal`&&(0,x.jsxs)(A,{error:!0,children:[`对话模型:`,D(m.state),`。`,m.last_error?.includes(`未配置`)?`未配置模型,暂不学习。`:`学习已暂停。`,`消息仍可接收。`,(0,x.jsx)(`a`,{href:`#/status`,children:`查看运行状态`})]}),u===`settings`?(0,x.jsx)(Ne,{}):u===`memories`?(0,x.jsx)(on,{initialQuery:t.split(`?`)[1],version:a,openMemory:f,onChange:d},t):u===`people-graph`?(0,x.jsx)(gi,{initialQuery:t.split(`?`)[1],openMemory:f},t):u===`people`?(0,x.jsx)(ei,{openMemory:f,initialQuery:t.split(`?`)[1],onChange:()=>{c.refresh(),d()}},t):u===`operations`?(0,x.jsx)(we,{initialQuery:t.split(`?`)[1]},t):u===`learning`?(0,x.jsx)(Br,{openMemory:f}):u===`state`?(0,x.jsx)(Pn,{initialQuery:t.split(`?`)[1],openMemory:f},t):u===`persona`?(0,x.jsx)(wi,{initialQuery:t.split(`?`)[1],openMemory:f},t):u===`connections`?(0,x.jsx)(Ue,{}):u===`status`?(0,x.jsx)(Yn,{initialQuery:t.split(`?`)[1]||``,data:s.data,openMemory:f},t):(0,x.jsx)(In,{status:s.data,openMemory:f,refreshStatus:s.refresh})]})}),(0,x.jsx)(`footer`,{className:`app-footer`,children:`Iris · 记忆有来源,相处有连续`}),r!==null&&(0,x.jsx)(cn,{id:r,onClose:p,openMemory:f,onChange:d},r)]})}(0,c.createRoot)(document.getElementById(`root`)).render((0,x.jsx)(l.StrictMode,{children:(0,x.jsx)(Di,{})})); \ No newline at end of file +`).replace(Zf,``)}function $f(e,t){return t=Qf(t),Qf(e)===t}function ep(e,t,n,r,i,a){switch(n){case`children`:if(typeof r==`string`)t===`body`||t===`textarea`&&r===``||an(e,r);else if(typeof r==`number`||typeof r==`bigint`)t!==`body`&&an(e,``+r);else return;break;case`className`:Ut(e,`class`,r);break;case`tabIndex`:Ut(e,`tabindex`,r);break;case`dir`:case`role`:case`viewBox`:case`width`:case`height`:Ut(e,n,r);break;case`style`:cn(e,r,a);return;case`data`:if(t!==`object`){Ut(e,`data`,r);break}case`src`:case`href`:if(r===``&&(t!==`a`||n!==`href`)){e.removeAttribute(n);break}if(r==null||typeof r==`function`||typeof r==`symbol`||typeof r==`boolean`){e.removeAttribute(n);break}r=fn(r),e.setAttribute(n,r);break;case`action`:case`formAction`:if(typeof r==`function`){e.setAttribute(n,`javascript:throw new Error('A React form was unexpectedly submitted. If you called form.submit() manually, consider using form.requestSubmit() instead. If you\\'re trying to use event.stopPropagation() in a submit event handler, consider also calling event.preventDefault().')`);break}if(typeof a==`function`&&(n===`formAction`?(t!==`input`&&ep(e,t,`name`,i.name,i,null),ep(e,t,`formEncType`,i.formEncType,i,null),ep(e,t,`formMethod`,i.formMethod,i,null),ep(e,t,`formTarget`,i.formTarget,i,null)):(ep(e,t,`encType`,i.encType,i,null),ep(e,t,`method`,i.method,i,null),ep(e,t,`target`,i.target,i,null))),r==null||typeof r==`symbol`||typeof r==`boolean`){e.removeAttribute(n);break}r=fn(r),e.setAttribute(n,r);break;case`onClick`:r!=null&&(e.onclick=pn);return;case`onScroll`:r!=null&&$(`scroll`,e);return;case`onScrollEnd`:r!=null&&$(`scrollend`,e);return;case`dangerouslySetInnerHTML`:if(r!=null){if(typeof r!=`object`||!(`__html`in r))throw Error(s(61));if(n=r.__html,n!=null){if(i.children!=null)throw Error(s(60));a?.__html!==n&&(e.innerHTML=n)}}break;case`multiple`:e.multiple=r&&typeof r!=`function`&&typeof r!=`symbol`;break;case`muted`:e.muted=r&&typeof r!=`function`&&typeof r!=`symbol`;break;case`suppressContentEditableWarning`:case`suppressHydrationWarning`:case`defaultValue`:case`defaultChecked`:case`innerHTML`:case`ref`:break;case`autoFocus`:break;case`xlinkHref`:if(r==null||typeof r==`function`||typeof r==`boolean`||typeof r==`symbol`){e.removeAttribute(`xlink:href`);break}n=fn(r),e.setAttributeNS(`http://www.w3.org/1999/xlink`,`xlink:href`,n);break;case`contentEditable`:case`spellCheck`:case`draggable`:case`value`:case`autoReverse`:case`externalResourcesRequired`:case`focusable`:case`preserveAlpha`:r!=null&&typeof r!=`function`&&typeof r!=`symbol`?e.setAttribute(n,r):e.removeAttribute(n);break;case`inert`:case`allowFullScreen`:case`async`:case`autoPlay`:case`controls`:case`credentialless`:case`default`:case`defer`:case`disabled`:case`disablePictureInPicture`:case`disableRemotePlayback`:case`formNoValidate`:case`hidden`:case`loop`:case`noModule`:case`noValidate`:case`open`:case`playsInline`:case`readOnly`:case`required`:case`reversed`:case`scoped`:case`seamless`:case`itemScope`:r&&typeof r!=`function`&&typeof r!=`symbol`?e.setAttribute(n,``):e.removeAttribute(n);break;case`capture`:case`download`:!0===r?e.setAttribute(n,``):!1!==r&&r!=null&&typeof r!=`function`&&typeof r!=`symbol`?e.setAttribute(n,r):e.removeAttribute(n);break;case`cols`:case`rows`:case`size`:case`span`:r!=null&&typeof r!=`function`&&typeof r!=`symbol`&&!isNaN(r)&&1<=r?e.setAttribute(n,r):e.removeAttribute(n);break;case`rowSpan`:case`start`:r==null||typeof r==`function`||typeof r==`symbol`||isNaN(r)?e.removeAttribute(n):e.setAttribute(n,r);break;case`popover`:$(`beforetoggle`,e),$(`toggle`,e),Ht(e,`popover`,r);break;case`xlinkActuate`:Wt(e,`http://www.w3.org/1999/xlink`,`xlink:actuate`,r);break;case`xlinkArcrole`:Wt(e,`http://www.w3.org/1999/xlink`,`xlink:arcrole`,r);break;case`xlinkRole`:Wt(e,`http://www.w3.org/1999/xlink`,`xlink:role`,r);break;case`xlinkShow`:Wt(e,`http://www.w3.org/1999/xlink`,`xlink:show`,r);break;case`xlinkTitle`:Wt(e,`http://www.w3.org/1999/xlink`,`xlink:title`,r);break;case`xlinkType`:Wt(e,`http://www.w3.org/1999/xlink`,`xlink:type`,r);break;case`xmlBase`:Wt(e,`http://www.w3.org/XML/1998/namespace`,`xml:base`,r);break;case`xmlLang`:Wt(e,`http://www.w3.org/XML/1998/namespace`,`xml:lang`,r);break;case`xmlSpace`:Wt(e,`http://www.w3.org/XML/1998/namespace`,`xml:space`,r);break;case`is`:Ht(e,`is`,r);break;case`innerText`:case`textContent`:return;default:if(!(2s)break;var u=c.transferSize,d=c.initiatorType;u&&ap(d)&&(c=c.responseEnd,o+=u*(c$m){s.length=o;break}f=new Promise(jp.bind(f)),s.push(f)}}}if(0`u`?null:document;function Tm(e,t,n){var r=wm;if(r&&typeof t==`string`&&t){var i=Zt(t);i=`link[rel="`+e+`"][href="`+i+`"]`,typeof n==`string`&&(i+=`[crossorigin="`+n+`"]`),ym.has(i)||(ym.add(i),e={rel:e,crossOrigin:n,href:t},r.querySelector(i)===null&&(t=r.createElement(`link`),np(t,`link`,e),jt(t),r.head.appendChild(t)))}}function Em(e){xm.D(e),Tm(`dns-prefetch`,e,null)}function Dm(e,t){xm.C(e,t),Tm(`preconnect`,e,t)}function Om(e,t,n){xm.L(e,t,n);var r=wm;if(r&&e&&t){var i=`link[rel="preload"][as="`+Zt(t)+`"]`;t===`image`&&n&&n.imageSrcSet?(i+=`[imagesrcset="`+Zt(n.imageSrcSet)+`"]`,typeof n.imageSizes==`string`&&(i+=`[imagesizes="`+Zt(n.imageSizes)+`"]`)):i+=`[href="`+Zt(e)+`"]`;var a=i;switch(t){case`style`:a=Pm(e);break;case`script`:a=Rm(e)}if(!(vm.has(a)||(e=D({rel:`preload`,href:t===`image`&&n&&n.imageSrcSet?void 0:e,as:t},n),vm.set(a,e),r.querySelector(i)!==null||t===`style`&&r.querySelector(Fm(a))||t===`script`&&r.querySelector(zm(a))))){var o=r.createElement(`link`);np(o,`link`,e),t===`style`&&(o[Tt]=!0,o.onload=o.onerror=function(){Mt(o)}),jt(o),r.head.appendChild(o)}}}function km(e,t){xm.m(e,t);var n=wm;if(n&&e){var r=t&&typeof t.as==`string`?t.as:`script`,i=`link[rel="modulepreload"][as="`+Zt(r)+`"][href="`+Zt(e)+`"]`,a=i;switch(r){case`audioworklet`:case`paintworklet`:case`serviceworker`:case`sharedworker`:case`worker`:case`script`:a=Rm(e)}if(!vm.has(a)&&(e=D({rel:`modulepreload`,href:e},t),vm.set(a,e),n.querySelector(i)===null)){switch(r){case`audioworklet`:case`paintworklet`:case`serviceworker`:case`sharedworker`:case`worker`:case`script`:if(n.querySelector(zm(a)))return}r=n.createElement(`link`),np(r,`link`,e),jt(r),n.head.appendChild(r)}}}function Am(e,t,n){xm.S(e,t,n);var r=wm;if(r&&e){var i=At(r).hoistableStyles,a=Pm(e);t||=`default`;var o=i.get(a);if(!o){var s={loading:0,preload:null};if(o=r.querySelector(Fm(a)))s.loading=5;else{e=D({rel:`stylesheet`,href:e,"data-precedence":t},n),(n=vm.get(a))&&Hm(e,n);var c=o=r.createElement(`link`);jt(c),np(c,`link`,e),c._p=new Promise(function(e,t){c.onload=e,c.onerror=t}),c.addEventListener(`load`,function(){s.loading|=1}),c.addEventListener(`error`,function(){s.loading|=2}),s.loading|=4,Vm(o,t,r)}o={type:`stylesheet`,instance:o,count:1,state:s},i.set(a,o)}}}function jm(e,t){xm.X(e,t);var n=wm;if(n&&e){var r=At(n).hoistableScripts,i=Rm(e),a=r.get(i);a||(a=n.querySelector(zm(i)),a||(e=D({src:e,async:!0},t),(t=vm.get(i))&&Um(e,t),a=n.createElement(`script`),jt(a),np(a,`link`,e),n.head.appendChild(a)),a={type:`script`,instance:a,count:1,state:null},r.set(i,a))}}function Mm(e,t){xm.M(e,t);var n=wm;if(n&&e){var r=At(n).hoistableScripts,i=Rm(e),a=r.get(i);a||(a=n.querySelector(zm(i)),a||(e=D({src:e,async:!0,type:`module`},t),(t=vm.get(i))&&Um(e,t),a=n.createElement(`script`),jt(a),np(a,`link`,e),n.head.appendChild(a)),a={type:`script`,instance:a,count:1,state:null},r.set(i,a))}}function Nm(e,t,n,r){var i=(i=_e.current)?bm(i):null;if(!i)throw Error(s(446));switch(e){case`meta`:case`title`:return null;case`style`:return typeof n.precedence==`string`&&typeof n.href==`string`?(n=Pm(n.href),t=At(i).hoistableStyles,r=t.get(n),r||(r={type:`style`,instance:null,count:0,state:null},t.set(n,r)),r):{type:`void`,instance:null,count:0,state:null};case`link`:if(n.rel===`stylesheet`&&typeof n.href==`string`&&typeof n.precedence==`string`){e=Pm(n.href);var a=At(i).hoistableStyles,o=a.get(e);if(o||(i=i.ownerDocument||i,o={type:`stylesheet`,instance:null,count:0,state:{loading:0,preload:null}},a.set(e,o),(a=i.querySelector(Fm(e)))?a._p||(o.instance=a,o.state.loading=5):(a=vm.get(e),a||(a={rel:`preload`,as:`style`,href:n.href,crossOrigin:n.crossOrigin,integrity:n.integrity,media:n.media,hrefLang:n.hrefLang,referrerPolicy:n.referrerPolicy},vm.set(e,a)),Lm(i,e,a,o.state))),t&&r===null)throw Error(s(528,``));return o}if(t&&r!==null)throw Error(s(529,``));return null;case`script`:return t=n.async,n=n.src,typeof n==`string`&&t&&typeof t!=`function`&&typeof t!=`symbol`?(n=Rm(n),t=At(i).hoistableScripts,r=t.get(n),r||(r={type:`script`,instance:null,count:0,state:null},t.set(n,r)),r):{type:`void`,instance:null,count:0,state:null};default:throw Error(s(444,e))}}function Pm(e){return`href="`+Zt(e)+`"`}function Fm(e){return`link[rel="stylesheet"][`+e+`]`}function Im(e){return D({},e,{"data-precedence":e.precedence,precedence:null})}function Lm(e,t,n,r){if(t=e.querySelector(`link[rel="preload"][as="style"][`+t+`]`)){if(!0!==t[Tt]){r.loading=1;return}}else t=e.createElement(`link`),t[Tt]=!0,t.onload=t.onerror=Mt.bind(null,t),np(t,`link`,n),jt(t),e.head.appendChild(t);r.preload=t,t.addEventListener(`load`,function(){return r.loading|=1}),t.addEventListener(`error`,function(){return r.loading|=2})}function Rm(e){return`[src="`+Zt(e)+`"]`}function zm(e){return`script[async]`+e}function Bm(e,t,n){if(t.count++,t.instance===null)switch(t.type){case`style`:var r=e.querySelector(`style[data-href~="`+Zt(n.href)+`"]`);if(r)return t.instance=r,jt(r),r;var i=D({},n,{"data-href":n.href,"data-precedence":n.precedence,href:null,precedence:null});return r=(e.ownerDocument||e).createElement(`style`),jt(r),np(r,`style`,i),Vm(r,n.precedence,e),t.instance=r;case`stylesheet`:i=Pm(n.href);var a=e.querySelector(Fm(i));if(a)return t.state.loading|=4,t.instance=a,jt(a),a;r=Im(n),(i=vm.get(i))&&Hm(r,i),a=(e.ownerDocument||e).createElement(`link`),jt(a);var o=a;return o._p=new Promise(function(e,t){o.onload=e,o.onerror=t}),np(a,`link`,r),t.state.loading|=4,Vm(a,n.precedence,e),t.instance=a;case`script`:return a=Rm(n.src),(i=e.querySelector(zm(a)))?(t.instance=i,jt(i),i):(r=n,(i=vm.get(a))&&(r=D({},n),Um(r,i)),e=e.ownerDocument||e,i=e.createElement(`script`),jt(i),np(i,`link`,r),e.head.appendChild(i),t.instance=i);case`void`:return null;default:throw Error(s(443,t.type))}else t.type===`stylesheet`&&!(t.state.loading&4)&&(r=t.instance,t.state.loading|=4,Vm(r,n.precedence,e));return t.instance}function Vm(e,t,n){for(var r=n.querySelectorAll(`link[rel="stylesheet"][data-precedence],style[data-precedence]`),i=r.length?r[r.length-1]:null,a=i,o=0;o title`):null)}function qm(e,t,n){if(n===1||t.itemProp!=null)return!1;switch(e){case`meta`:case`title`:return!0;case`style`:if(typeof t.precedence!=`string`||typeof t.href!=`string`||t.href===``)break;return!0;case`link`:if(typeof t.rel!=`string`||typeof t.href!=`string`||t.href===``||t.onLoad||t.onError)break;switch(t.rel){case`stylesheet`:return e=t.disabled,typeof t.precedence==`string`&&e==null;default:return!0}case`script`:if(t.async&&typeof t.async!=`function`&&typeof t.async!=`symbol`&&!t.onLoad&&!t.onError&&t.src&&typeof t.src==`string`)return!0}return!1}function Jm(e,t){return e===`img`&&t.src!=null&&t.src!==``&&t.onLoad==null&&t.loading!==`lazy`}function Ym(e){return!(e.type===`stylesheet`&&!(e.state.loading&3))}function Xm(e){return(e.width||100)*(e.height||100)*(typeof devicePixelRatio==`number`?devicePixelRatio:1)*.25}function Zm(e,t){typeof t.decode==`function`&&(e.imgCount++,t.complete||(e.imgBytes+=Xm(t),e.suspenseyImages.push(t)),e=rh.bind(e),t.decode().then(e,e))}function Qm(e,t,n,r){if(n.type===`stylesheet`&&(typeof r.media!=`string`||!1!==matchMedia(r.media).matches)&&!(n.state.loading&4)){if(n.instance===null){var i=Pm(r.href),a=t.querySelector(Fm(i));if(a){t=a._p,typeof t==`object`&&t&&typeof t.then==`function`&&(e.count++,e=nh.bind(e),t.then(e,e)),n.state.loading|=4,n.instance=a,jt(a);return}a=t.ownerDocument||t,r=Im(r),(i=vm.get(i))&&Hm(r,i),a=a.createElement(`link`),jt(a);var o=a;o._p=new Promise(function(e,t){o.onload=e,o.onerror=t}),np(a,`link`,r),n.instance=a}e.stylesheets===null&&(e.stylesheets=new Map),e.stylesheets.set(n,t),(t=n.state.preload)&&!(n.state.loading&3)&&(e.count++,n=nh.bind(e),t.addEventListener(`load`,n),t.addEventListener(`error`,n))}}var $m=0;function eh(e,t){return e.stylesheets&&e.count===0&&ah(e,e.stylesheets),0$m?50:800)+t);return e.unsuspend=n,function(){e.unsuspend=null,clearTimeout(r),clearTimeout(i)}}:null}function th(e){if(e.count===0&&(e.imgCount===0||!e.waitingForImages)){if(e.stylesheets)ah(e,e.stylesheets);else if(e.unsuspend){var t=e.unsuspend;e.unsuspend=null,t()}}}function nh(){this.count--,th(this)}function rh(){this.imgCount--,th(this)}var ih=null;function ah(e,t){e.stylesheets=null,e.unsuspend!==null&&(e.count++,ih=new Map,t.forEach(oh,e),ih=null,nh.call(e))}function oh(e,t){if(!(t.state.loading&4)){var n=ih.get(e);if(n)var r=n.get(null);else{n=new Map,ih.set(e,n);for(var i=e.querySelectorAll(`link[data-precedence],style[data-precedence]`),a=0;a{function n(){if(typeof __REACT_DEVTOOLS_GLOBAL_HOOK__<`u`&&typeof __REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE==`function`)try{__REACT_DEVTOOLS_GLOBAL_HOOK__.checkDCE(n)}catch(e){console.error(e)}}n(),t.exports=s()}))(),l=n(),u=``;function d(e){u=e}var f=class extends Error{code;status;result;constructor(e,t,n,r){super(t),this.code=e,this.status=n,this.result=r}};async function p(e,t){let n=await fetch(`/admin/api${e}`,{...t,credentials:`same-origin`,headers:{"Content-Type":`application/json; charset=utf-8`,...u?{"X-Iris-CSRF":u}:{},...t?.headers}});if(!n.ok){let e=await n.json().catch(()=>({})),t=e.error||{};throw[`login_required`,`setup_required`,`csrf_failed`].includes(t.code)&&dispatchEvent(new Event(`iris-auth`)),new f(t.code||`request_failed`,t.message||(t.fields?`请检查输入:${t.fields.map(e=>`${e.field.split(`.`).pop()}:${e.message}`).join(`;`)}`:`请求失败,请重试`),n.status,e.result)}return n}async function m(e,t){let n=await p(e,t);return{data:await n.json(),status:n.status,headers:n.headers}}async function h(e,t){let n=await p(e,t);if(n.headers.get(`Content-Type`)?.split(`;`)[0].trim()!==`application/zip`)throw new f(`invalid_download`,`没有收到 ZIP 备份,请刷新登录状态后重试`,n.status);return{blob:await n.blob(),headers:n.headers}}async function g(e,t){return(await m(e,t)).data}var _=(e,t)=>({method:e,...t===void 0?{}:{body:JSON.stringify(t)}}),v=e=>e instanceof f?e.status===410&&[`entry_profile_legacy_write_retired`,`entry_profile_publish_mode_retired`].includes(e.code)?`旧画像的整版编辑、回滚和候选审批已退役。请在入口画像页逐条管理画像记忆;旧版本仅供查看。`:e.message:`无法连接服务,请检查服务是否仍在运行`;function y(e,t=0){let[n,r]=(0,l.useState)(null),[i,a]=(0,l.useState)(``),[o,s]=(0,l.useState)(e!==null),[c,u]=(0,l.useState)(0),d=(0,l.useCallback)(()=>{s(!0),u(e=>e+1)},[]);return(0,l.useEffect)(()=>{r(null),a(``)},[e]),(0,l.useEffect)(()=>{if(s(e!==null),e===null)return;let n=new AbortController,i=!0,o,c=async()=>{try{let t=await g(e,{signal:n.signal});i&&(r(t),a(``))}catch(e){i&&!n.signal.aborted&&a(v(e))}finally{i&&s(!1)}i&&t&&(o=setTimeout(c,t))};return c(),()=>{i=!1,n.abort(),clearTimeout(o)}},[e,t,c]),{data:n,error:i,refresh:d,loading:o}}var b=e((e=>{var t=Symbol.for(`react.transitional.element`),n=Symbol.for(`react.fragment`);function r(e,n,r){var i=null;if(r!==void 0&&(i=``+r),n.key!==void 0&&(i=``+n.key),`key`in n)for(var a in r={},n)a!==`key`&&(r[a]=n[a]);else r=n;return n=r.ref,{$$typeof:t,type:e,key:i,ref:n===void 0?null:n,props:r}}e.Fragment=n,e.jsx=r,e.jsxs=r})),x=e(((e,t)=>{t.exports=b()}))(),S=e=>e?new Date(e).toLocaleString(`zh-CN`,{month:`numeric`,day:`numeric`,hour:`2-digit`,minute:`2-digit`}):`暂无记录`,C=e=>e==null?`—`:`${(e/1e3).toFixed(1)} 秒`,w=e=>e?new Date(e).toLocaleString(`zh-CN`):`暂无记录`;function T({total:e,offset:t,limit:n=30,change:r}){return e<=n&&t===0?null:(0,x.jsxs)(`div`,{className:`pagination`,children:[(0,x.jsx)(`button`,{className:`secondary`,disabled:t===0,onClick:()=>r(Math.max(0,t-n)),children:`上一页`}),(0,x.jsxs)(`span`,{children:[`第 `,Math.floor(t/n)+1,` 页 · 共 `,e,` 条`]}),(0,x.jsx)(`button`,{className:`secondary`,disabled:t+n>=e,onClick:()=>r(t+n),children:`下一页`})]})}var E=e=>({active:`有效`,forgotten:`已遗忘`,deleted:`已删除`})[e]||e,D=e=>({normal:`正常`,temporarily_unavailable:`暂时不可用`,invalid_key:`密钥无效`,configuration_error:`配置错误`,account_problem:`账户异常`,usage_limit:`达到今日用量上限`,rate_limited:`限流退避中`})[e]||e,O=e=>({pending:`已接收 · 待学习`,batched:`已接收 · 批次处理中`,learned:`学习已完成`,abandoned:`学习放弃 · 未记住`,refused:`内容拒绝 · 未记住`,filtered:`已过滤 · 不参与学习`})[e]||e;function k(e){return e?e.state===`succeeded`?e.result?.created?.length||e.result?.updated?.length?`学习成功 · 新增 ${e.result.created?.length||0} / 更新 ${e.result.updated?.length||0}`:`学习成功,未形成新记忆`:{running:`正在学习`,waiting:e.next_retry_at?`失败后等待重试`:`等待学习`,abandoned:`已放弃 · 未记住`,refused:`已拒绝 · 未记住`}[e.state]||e.state:`暂无批次`}function A({children:e,error:t=!1}){return(0,x.jsx)(`div`,{className:`notice ${t?`error`:``}`,role:t?`alert`:`status`,children:e})}function j({title:e,children:t}){return(0,x.jsxs)(`div`,{className:`empty`,children:[(0,x.jsx)(`span`,{className:`empty-mark`,"aria-hidden":!0,children:`✳`}),(0,x.jsx)(`h3`,{children:e}),t&&(0,x.jsx)(`p`,{children:t})]})}function M({children:e,tone:t=``}){return(0,x.jsx)(`span`,{className:`badge ${t}`,children:e})}function ee({title:e,onClose:t,children:n,closeDisabled:r=!1}){let i=(0,l.useRef)(null),a=(0,l.useRef)(t);return a.current=r?()=>{}:t,(0,l.useEffect)(()=>{let e=document.activeElement;(i.current?.querySelector(`button,input,textarea,select`))?.focus();let t=e=>{if(e.key===`Escape`&&(e.preventDefault(),a.current()),e.key===`Tab`){let t=[...i.current?.querySelectorAll(`button:not(:disabled),a[href],input:not(:disabled),textarea:not(:disabled),select:not(:disabled),[tabindex="0"]`)||[]],n=t[0],r=t[t.length-1];e.shiftKey&&document.activeElement===n?(e.preventDefault(),r?.focus()):!e.shiftKey&&document.activeElement===r&&(e.preventDefault(),n?.focus())}},n=document.body.style.overflow;return document.body.style.overflow=`hidden`,document.addEventListener(`keydown`,t),()=>{document.body.style.overflow=n,document.removeEventListener(`keydown`,t),e?.focus()}},[]),(0,x.jsx)(`div`,{className:`overlay`,children:(0,x.jsxs)(`section`,{ref:i,className:`dialog`,role:`dialog`,"aria-modal":`true`,"aria-label":e,children:[(0,x.jsxs)(`div`,{className:`dialog-title`,children:[(0,x.jsx)(`h2`,{children:e}),(0,x.jsx)(`button`,{className:`icon-button`,"aria-label":`关闭详情`,disabled:r,onClick:t,children:`×`})]}),n]})})}function N({value:e}){return e?(0,x.jsx)(`time`,{dateTime:e,children:e.replace(`T`,` `).replace(/\.\d+(?=[+-]|Z$)/,``).replace(/([+-]\d{2}:\d{2}|Z)$/,` $1`)}):(0,x.jsx)(x.Fragment,{children:`暂无记录`})}function P(e,t){return Object.fromEntries(Object.keys(t).filter(n=>!Object.is(e[n],t[n])).map(e=>[e,t[e]]))}function F(e){let[t,n]=(0,l.useState)({});function r(t,r){n(n=>{let i={...n};return Object.is(e[t],r)?delete i[t]:i[t]=r,i})}return{draft:{...e,...t},change:r,reset:()=>n({})}}function I({value:e,busy:t,saveError:n,nativeImagesAvailable:r,chatEnabled:i,external:a,save:o}){let s={daily_token_limit:e.daily_token_limit,learning_concurrency:e.learning_concurrency,native_image_learning:e.native_image_learning===!0,media_retention_days:e.media_retention_days??7},{draft:c,change:u,reset:d}=F({daily_token_limit:s.daily_token_limit?.toString()||``,learning_concurrency:String(s.learning_concurrency),native_image_learning:s.native_image_learning,media_retention_days:String(s.media_retention_days)}),[f,p]=(0,l.useState)(``),[m,h]=(0,l.useState)(!1);async function g(e){if(e.preventDefault(),t)return;p(``),h(!1);let n=c.daily_token_limit.trim()?Number(c.daily_token_limit):null,r=Number(c.learning_concurrency),i=Number(c.media_retention_days);if(n!==null&&(!Number.isInteger(n)||n<1)){p(`每日 token 上限须为正整数,留空表示不限`);return}if(!c.learning_concurrency.trim()||!Number.isInteger(r)||r<1||r>32){p(`学习并发须为 1—32 的整数`);return}if(!c.media_retention_days.trim()||!Number.isInteger(i)||i<1||i>36500){p(`原图保留天数须为 1—36500 的整数`);return}let a=P(s,{daily_token_limit:n,learning_concurrency:r,native_image_learning:c.native_image_learning,media_retention_days:i});Object.keys(a).length&&await o(a)&&(d(),h(!0))}return(0,x.jsxs)(`section`,{className:`panel settings-panel`,"aria-label":`用量与学习`,children:[(0,x.jsx)(`h2`,{children:`用量与学习`}),(0,x.jsx)(`form`,{onSubmit:g,noValidate:!0,onChange:()=>h(!1),children:(0,x.jsxs)(`fieldset`,{className:`lifecycle-fieldset`,disabled:t,children:[(0,x.jsxs)(`label`,{children:[`每日 token 上限`,(0,x.jsx)(`input`,{"aria-label":`每日 token 上限`,type:`number`,min:1,step:1,value:c.daily_token_limit,placeholder:`不限`,onChange:e=>u(`daily_token_limit`,e.target.value)})]}),(0,x.jsxs)(`label`,{children:[`学习并发`,(0,x.jsx)(`input`,{"aria-label":`学习并发`,type:`number`,required:!0,min:1,max:32,step:1,value:c.learning_concurrency,onChange:e=>u(`learning_concurrency`,e.target.value)})]}),(0,x.jsx)(`p`,{className:`muted`,children:`按角色时区每日恢复;在途调用和未报告的 token 可能使实际用量超过上限。`}),(0,x.jsxs)(`label`,{children:[`原图保留天数`,(0,x.jsx)(`input`,{"aria-label":`原图保留天数`,type:`number`,required:!0,min:1,max:36500,step:1,value:c.media_retention_days,onChange:e=>u(`media_retention_days`,e.target.value)}),(0,x.jsx)(`small`,{children:`1—36500 的整数,默认 7 天;与消息保留天数分别设置。`})]}),(0,x.jsx)(`p`,{className:`lifecycle-help`,children:`到期由每日维护只清理原图字节,理解文本保留。作为记忆来源的图片、正在理解的图片受保护。`}),(0,x.jsxs)(`label`,{className:`check-label`,children:[(0,x.jsx)(`input`,{type:`checkbox`,"aria-describedby":`native-image-learning-help native-image-learning-availability`,checked:c.native_image_learning,disabled:t||!r,onChange:e=>u(`native_image_learning`,e.target.checked)}),`学习时直接看原图`]}),(0,x.jsx)(`p`,{className:`lifecycle-help`,id:`native-image-learning-help`,children:`默认关闭。开启后,后续学习把原图交给对话模型;图片描述仍用于回复准备、检索与管理页。超限或不可用的图片沿用描述或占位。`}),(0,x.jsx)(`p`,{className:`lifecycle-help`,id:`native-image-learning-availability`,children:r?a?`这是运行设置,使用外部模型配置时也可以保存。`:`使用已保存的对话模型图片能力;保存后对后续学习生效。`:`${i?`当前对话模型未启用图片输入(vision),此设置暂不生效。`:`当前对话模型未启用,无法直接看原图。`}${a?`请在外部模型配置中启用对话模型及 vision。`:`请先启用并保存对话模型的“支持图片输入(vision)”。`}`}),(f||n)&&(0,x.jsx)(A,{error:!0,children:f||n}),m&&(0,x.jsx)(A,{children:`用量与学习设置已保存`}),(0,x.jsx)(`button`,{className:`primary`,children:`保存用量与学习`})]})})]})}function te({value:e,busy:t,saveError:n,save:r}){let i={...e,internal_question_expiry_days:e.internal_question_expiry_days??7,goal_auto_review_enabled:e.goal_auto_review_enabled??!1,goal_duplicate_auto_judge_enabled:e.goal_duplicate_auto_judge_enabled??!1},{draft:a,change:o,reset:s}=F({default_reminder_minutes:String(i.default_reminder_minutes),overdue_reminders:i.overdue_reminders,internal_question_expiry_days:String(i.internal_question_expiry_days),goal_auto_review_enabled:i.goal_auto_review_enabled,goal_duplicate_auto_judge_enabled:i.goal_duplicate_auto_judge_enabled}),[c,u]=(0,l.useState)(``),[d,f]=(0,l.useState)(!1);async function p(e){if(e.preventDefault(),t)return;u(``),f(!1);let n=Number(a.default_reminder_minutes),o=Number(a.internal_question_expiry_days);if(!a.default_reminder_minutes.trim()||!Number.isInteger(n)||n<0||n>525600){u(`默认提醒提前量须为 0—525600 的整数`);return}if(!a.internal_question_expiry_days.trim()||!Number.isInteger(o)||o<1||o>365){u(`学习产生的询问保留天数须为 1—365 的整数`);return}let c=P(i,{default_reminder_minutes:n,overdue_reminders:a.overdue_reminders,internal_question_expiry_days:o,goal_auto_review_enabled:a.goal_auto_review_enabled,goal_duplicate_auto_judge_enabled:a.goal_duplicate_auto_judge_enabled});Object.keys(c).length&&await r(c)&&(s(),f(!0))}return(0,x.jsxs)(`section`,{className:`panel settings-panel`,"aria-label":`目标与提醒`,children:[(0,x.jsx)(`h2`,{children:`目标与提醒`}),(0,x.jsx)(`p`,{children:`未单独填写提前量的普通目标使用默认值。询问不安排提醒,过期不会自动放弃目标。`}),(0,x.jsx)(`form`,{onSubmit:p,noValidate:!0,onChange:()=>f(!1),children:(0,x.jsxs)(`fieldset`,{className:`lifecycle-fieldset`,disabled:t,children:[(0,x.jsxs)(`label`,{children:[`默认提醒提前量(分钟)`,(0,x.jsx)(`input`,{"aria-label":`默认提醒提前量(分钟)`,type:`number`,min:0,max:525600,step:1,required:!0,value:a.default_reminder_minutes,onChange:e=>o(`default_reminder_minutes`,e.target.value)}),(0,x.jsx)(`small`,{children:`0—525600 的整数,默认 60 分钟;0 表示到期时提醒。`})]}),(0,x.jsxs)(`label`,{className:`check-label`,children:[(0,x.jsx)(`input`,{type:`checkbox`,checked:a.overdue_reminders,onChange:e=>o(`overdue_reminders`,e.target.checked)}),`启用过期提醒`]}),(0,x.jsxs)(`p`,{className:`lifecycle-help`,children:[`过期提醒每日最多一次。`,(0,x.jsx)(`a`,{href:`#/state?tab=notifications`,children:`查看提醒`}),`;“已取走”只表示宿主拿到了。`]}),(0,x.jsxs)(`label`,{children:[`学习产生的询问保留天数`,(0,x.jsx)(`input`,{"aria-label":`学习产生的询问保留天数`,type:`number`,min:1,max:365,step:1,required:!0,value:a.internal_question_expiry_days,onChange:e=>o(`internal_question_expiry_days`,e.target.value)}),(0,x.jsx)(`small`,{children:`1—365 的整数,默认 7 天。`})]}),(0,x.jsx)(`p`,{className:`lifecycle-help`,children:`只作用于学习产生且仍未结束的询问,到期由每日维护自动结束。宿主或管理员创建的询问和普通目标不受影响。`}),(0,x.jsxs)(`label`,{className:`check-label`,children:[(0,x.jsx)(`input`,{type:`checkbox`,checked:a.goal_auto_review_enabled,onChange:e=>o(`goal_auto_review_enabled`,e.target.checked)}),`每日维护按依据自动完成或放弃内部目标`]}),(0,x.jsx)(`p`,{className:`lifecycle-help`,children:`只处理学习产生、仍未结束的普通目标。模型给出依据消息,程序核对依据晚于目标创建、来自涉及的人或角色自己后才关闭;含糊或部分完成的说法保持未结束。宿主和管理员创建的目标不受影响,自动关闭的目标可以在目标页重开。默认关闭。`}),(0,x.jsxs)(`label`,{className:`check-label`,children:[(0,x.jsx)(`input`,{type:`checkbox`,checked:a.goal_duplicate_auto_judge_enabled,onChange:e=>o(`goal_duplicate_auto_judge_enabled`,e.target.checked)}),`每日维护补判“可能重复”的目标`]}),(0,x.jsx)(`p`,{className:`lifecycle-help`,children:`使用目标去重判断模型,每对最多 60 秒:判为重复时合并到较早的一条(含管理员创建的目标时不合并、改为撤销标记),判为不重复时撤销标记,仍不确定时保留标记。默认关闭。`}),(c||n)&&(0,x.jsx)(A,{error:!0,children:c||n}),d&&(0,x.jsx)(A,{children:`目标与提醒设置已保存`}),(0,x.jsx)(`button`,{className:`primary`,children:`保存目标与提醒设置`})]})})]})}var L={initial_setting:`初始设定`,periodic:`定期更新`,regenerate:`重新生成`,admin_edit:`管理员编辑`,rollback:`回滚`},ne={current:`当前`,pending:`待确认`,rejected:`已拒绝`,superseded:`已被后续候选替代`,history:`历史已发布`},R={small:`小`,medium:`中`,large:`大`},re={memory:`记忆依据`,admin:`手写`,initial_template:`初始模板`},ie={modified:`记忆已修改`,forgotten:`记忆已遗忘`,deleted:`记忆已删除或清除`,no_longer_self_memory:`已不属于自我记忆`,sources_changed:`来源已变化`,daily_token_limit:`达到每日用量上限`,no_self_evidence:`暂无有效自我记忆依据`,not_due:`尚未满足定期更新条件`,evidence_changed:`依据已变化`,current_version_changed:`当前版本已变化`,interrupted:`服务中断,任务未完成`,worker_unavailable:`生成工作线程不可用`,unconfigured:`尚未配置生成模型`,unexpected_error:`生成时发生异常`,timeout:`模型调用超时`,invalid_output:`模型输出不符合要求`,paused:`模型用途已暂停`,usage_limit:`达到每日用量上限`,temporarily_unavailable:`模型暂时不可用`,configuration_error:`模型配置错误`,invalid_key:`模型密钥无效`,account_problem:`模型账户异常`,rate_limited:`模型限流`,"administrator rejected":`管理员拒绝采用`,"sentences must be an array":`句子列表格式无效`,"empty persona":`persona 正文为空`,"persona exceeds 800 characters":`persona 正文超过 800 字`,"residual memory or participant reference":`正文残留记忆或参与者编号`,"under 300 characters: concise evidence-bound text is allowed":`不足 300 字:允许依据有限时保持简短`,"initial template retains the pre-M3 length behavior":`初始模板沿用此前的长度规则`,"nonempty text required":`句子不能为空`,"one sentence per item required":`每项须为一句话`,"invalid administrator attribution":`手写归属无效`,"missing or unknown evidence":`缺少依据或依据未知`,"invalid model verdict":`模型判断格式无效`,"single-date/unknown-date evidence needs a scene qualifier":`单日或日期未知的依据需要场景限定`,"invalid change degree":`变化程度无效`,"model check must cover every sentence, in order":`模型检查须按顺序覆盖每句话`,"model check reason required":`模型检查缺少理由`};function ae(e){let t=e.match(/^sentence (\d+):\s*(.*)$/);return t?`第 ${t[1]} 句:${ae(t[2])}`:ie[e]||e}var oe=e=>e===`queued`||e===`running`;function se(e,t){return oe(e)?{queued:`等待生成`,generating:`正在生成候选`,checking:`正在检查候选`}[t]||`正在生成候选`:{current:`已发布 persona`,pending:`候选待确认,当前版本仍保留`,rejected:`候选被拒绝,当前版本仍保留`,skipped:`本次未生成候选`,conflict:`生成期间版本或依据发生变化,未发布`,failed:`生成失败,当前版本仍保留`}[e]||e}var ce={disputed:`争议`,insufficient_support:`依据不足`,superseded:`已被替代`},le={pending:`待确认`,confirmed:`已确认`,cleared:`已清除`},ue=[[`max_calls`,`记忆整理(合并/矛盾/依赖)每轮调用上限`,300],[`entry_profile_max_calls`,`入口画像每轮调用上限`,60],[`person_summary_max_calls`,`人物摘要每轮调用上限`,40],[`persona_max_calls`,`persona 每轮调用上限`,30]],de=[[`report_only_v1`,`只报告:不改动记忆,由管理员决定`],[`auto_v1`,`自主处理(默认):模型判断,程序按规则执行,事后可撤销`]],z={supersedes:`本人已更正,旧记忆进入遗忘`,reported_vs_self:`本人说法与转述冲突,转述降低一档`,coexist:`历史与当前并存,不作处理`,undecided:`无法判定,两条都保留并标注争议`},B={forget:`没有独立依据,进入遗忘`,keep:`仍有独立依据,不作改动`,undecided:`复核依据未通过校验,本次不处理`},V={memory:`记忆整理(合并/矛盾/依赖)`,entry_profile:`入口画像`,person_summary:`人物摘要`,persona:`persona`},fe=[[`merge_enabled`,`合并重复记忆`],[`conflict_enabled`,`矛盾检查与建议`],[`dependency_enabled`,`派生依据复核与建议`],[`persona_enabled`,`定期更新 persona`],[`person_summary_enabled`,`定期更新人物摘要`],[`goal_review_enabled`,`目标依据复核`]];function pe(e){return{disabled:`本次设置已关闭`,protected:`人工编辑或置顶的记忆,未自动处理`,superseded:`已被替代的记忆,等管理员恢复后才恢复有效`,superseded_member:`含已被替代且遗忘的记忆,不合并也不建议`,missing_unique_detail:`合并正文缺少某条独有细节`,numbers_changed:`合并正文的数字与原文不一致`,negation_changed:`合并正文的否定与原文不一致`,qualifiers_changed:`合并正文的限定词与原文不一致`,subject_mentions_changed:`合并正文的主体引用与原文不一致`,time_expressions_changed:`合并正文的时间表达与原文不一致`,entity_lost:`合并正文丢失了人名或实体`,entity_added:`合并正文新增了人名或实体`,new_content_word:`合并正文出现了原文没有的词`,content_word_lost:`合并正文缺少原文的词`,attribution_differs:`两条的说话人、立场或涉及人不同`,event_time_differs:`两条的事件时间不同`,visibility_differs:`两条的可见范围不同`,merge_not_allowed:`两条不符合合并条件`,entry_profile_kind:`入口画像不做正文合并`,invalid_reason:`模型理由未通过校验`,model_items_disabled:`记忆模型整理项目均已关闭`,usage_limit:`每日 token 上限已用尽`,paused:`模型用途已暂停`,interrupted:`运行已中断`,call_budget:`模型调用次数预算已用尽`,persona_busy:`已有 persona 任务正在运行`,checks_rejected:`persona 检查未通过,候选被拒绝`,revision_conflict:`对象修订已变化,跳过本次写入`,unsafe_write:`未通过写入保护校验`,invalid_response:`模型返回格式无效`,invalid_json:`模型返回的 JSON 无效`,temporary_unavailable:`模型暂时不可用`,cooldown:`尚未到定期更新时间`,no_changes:`没有需要更新的变化`,stale:`判断期间目标或依据已变化,等待重新复核`,lease_expired:`本次判断任务已超时,等待重新复核`,configuration_changed:`判断设置已变化,等待重新复核`,goal_closed:`目标已结束,取消待复核任务`,in_progress:`已有复核任务正在运行`,queue_full:`判断排队已满`,configuration:`模型尚未配置或配置无效`,budget:`预算已耗尽`,invalid:`模型输出未通过校验`,source_unavailable:`来源已不可用`,insufficient_new_memories:`新增自我记忆不足,尚未到更新条件`,too_soon:`距离上次更新尚不足一个周期`}[e]||ae(e)}var me=e=>({...e,person_summary_enabled:e.person_summary_enabled??!0,resolution:e.resolution??`report_only_v1`,entry_profile_max_calls:e.entry_profile_max_calls??60,person_summary_max_calls:e.person_summary_max_calls??40,persona_max_calls:e.persona_max_calls??30});function he({value:e,timezone:t,busy:n,saveError:r,save:i}){let a=me(e),{draft:o,change:s,reset:c}=F({...a,max_calls:String(a.max_calls),entry_profile_max_calls:String(a.entry_profile_max_calls),person_summary_max_calls:String(a.person_summary_max_calls),persona_max_calls:String(a.persona_max_calls)}),[u,d]=(0,l.useState)(``),[f,p]=(0,l.useState)(!1);async function m(e){if(e.preventDefault(),n)return;if(d(``),p(!1),!/^(?:[01][0-9]|2[0-3]):[0-5][0-9]$/.test(o.maintenance_time)){d(`整理运行时间须为 HH:MM,范围 00:00—23:59`);return}for(let[e,t]of ue){let n=Number(o[e]);if(!o[e].trim()||!Number.isInteger(n)||n<0||n>1e3){d(`${t}须为 0—1000 的整数`);return}}let t=P(a,{...o,max_calls:Number(o.max_calls),entry_profile_max_calls:Number(o.entry_profile_max_calls),person_summary_max_calls:Number(o.person_summary_max_calls),persona_max_calls:Number(o.persona_max_calls)});Object.keys(t).length&&await i(t)&&(c(),p(!0))}return(0,x.jsxs)(`section`,{className:`panel settings-panel`,"aria-label":`梦境整理设置`,id:`consolidation-settings`,children:[(0,x.jsx)(`h2`,{children:`梦境整理`}),(0,x.jsx)(`p`,{className:`lifecycle-help`,children:`整理不中断接收和学习。修改只影响之后接受的新运行,已接受的运行与续跑沿用其设置快照。`}),(0,x.jsx)(`form`,{onSubmit:m,noValidate:!0,onChange:()=>p(!1),children:(0,x.jsxs)(`fieldset`,{disabled:n,className:`lifecycle-fieldset`,children:[(0,x.jsxs)(`label`,{children:[`整理运行时间`,(0,x.jsx)(`input`,{"aria-label":`整理运行时间`,type:`text`,required:!0,maxLength:5,placeholder:`03:00`,value:o.maintenance_time,onChange:e=>s(`maintenance_time`,e.target.value)}),(0,x.jsxs)(`small`,{children:[`HH:MM · 角色时区 `,t||`未设置`,`,与每日记忆维护共用时间。`]})]}),(0,x.jsx)(`div`,{className:`form-grid`,children:ue.map(([e,t,n])=>(0,x.jsxs)(`label`,{children:[t,(0,x.jsx)(`input`,{"aria-label":t,type:`number`,min:0,max:1e3,step:1,required:!0,value:o[e],onChange:t=>s(e,t.target.value)}),(0,x.jsxs)(`small`,{children:[`0—1000 的整数,默认 `,n,` 次。`]})]},e))}),(0,x.jsx)(`p`,{className:`lifecycle-help`,children:`以上均为每轮调用上限,各步骤互不占用;HTTP 重试和 JSON 修正计入所属步骤。0 表示停用该步骤的模型调用,其他步骤、记忆生命周期维护与开启的目标依据复核仍会执行。`}),fe.map(([e,t])=>(0,x.jsxs)(`label`,{className:`check-label`,children:[(0,x.jsx)(`input`,{type:`checkbox`,checked:o[e],onChange:t=>s(e,t.target.checked)}),t]},e)),(0,x.jsx)(`p`,{className:`lifecycle-help`,children:`persona 与人物摘要需满足各自更新条件且在各自上限内至少剩余 2 次调用名额。人物摘要开关只影响之后的整理,不关闭已发布摘要的宿主读取。`}),(0,x.jsxs)(`label`,{children:[`矛盾、依赖与互有细节合并的处理方式`,(0,x.jsx)(`select`,{"aria-label":`矛盾、依赖与互有细节合并的处理方式`,value:o.resolution,onChange:e=>s(`resolution`,e.target.value),children:de.map(([e,t])=>(0,x.jsx)(`option`,{value:e,children:t},e))})]}),(0,x.jsxs)(`p`,{className:`lifecycle-help`,children:[o.resolution===`auto_v1`?`自主处理:模型只给判断和合并草稿,分数、遗忘与标注由程序按规则写入;人工编辑和置顶的记忆不动,每项都有操作记录和撤销入口。`:`只报告:矛盾和依赖复核只产生管理员可见的模型建议,不改动记忆。`,`目标依据复核不自动完成或放弃目标。`]}),(u||r)&&(0,x.jsx)(A,{error:!0,children:u||r}),f&&(0,x.jsx)(A,{children:`梦境整理设置已保存`}),(0,x.jsxs)(`div`,{className:`actions`,children:[(0,x.jsx)(`button`,{className:`primary`,children:`保存梦境整理设置`}),(0,x.jsx)(`button`,{type:`button`,onClick:()=>{c(),d(``),p(!1)},children:`还原整理设置`})]})]})}),(0,x.jsx)(`a`,{href:`#/status`,children:`查看整理报告或手动运行`})]})}function H({value:e,busy:t,save:n}){let r=e=>({enabled:e.enabled,budget_seconds:String(e.budget_seconds),concurrency:String(e.concurrency),queue_limit:String(e.queue_limit)}),[i,a]=(0,l.useState)(()=>r(e)),o=(0,l.useRef)(!1),[s,c]=(0,l.useState)(``),[u,d]=(0,l.useState)(!1);(0,l.useEffect)(()=>{o.current||a(r(e))},[e]);function f(e,t){o.current=!0,d(!1),a(n=>({...n,[e]:t}))}async function p(e){if(e.preventDefault(),t)return;c(``),d(!1);let s=Number(i.budget_seconds),l=Number(i.concurrency),u=Number(i.queue_limit);if(!i.budget_seconds.trim()||!Number.isFinite(s)||s<=0||s>10){c(`目标去重判断预算须大于 0 且不超过 10 秒`);return}if(!i.concurrency.trim()||!Number.isInteger(l)||l<1||l>8){c(`目标去重判断并发须为 1—8 的整数`);return}if(!i.queue_limit.trim()||!Number.isInteger(u)||u<0||u>64){c(`目标去重判断排队上限须为 0—64 的整数`);return}let f=await n({enabled:i.enabled,budget_seconds:s,concurrency:l,queue_limit:u});f&&(o.current=!1,a(r(f)),d(!0))}return(0,x.jsxs)(`form`,{className:`judge-settings`,onSubmit:p,noValidate:!0,"aria-label":`目标去重判断设置`,children:[(0,x.jsx)(`h3`,{children:`目标判断开关与排队`}),(0,x.jsxs)(`fieldset`,{disabled:t,className:`lifecycle-fieldset`,children:[(0,x.jsxs)(`label`,{className:`check-label`,children:[(0,x.jsx)(`input`,{type:`checkbox`,checked:i.enabled,onChange:e=>f(`enabled`,e.target.checked)}),`启用目标去重判断`]}),(0,x.jsxs)(`label`,{children:[`目标去重方法`,(0,x.jsx)(`input`,{"aria-label":`目标去重方法`,readOnly:!0,value:i.enabled?`C2(批量判断与可能重复提示)`:`A(确定性规则)`}),(0,x.jsx)(`small`,{children:`方法由后端固定;管理接口目前不提供方法切换。`})]}),(0,x.jsx)(`p`,{className:`lifecycle-help`,children:`关闭后使用确定性规则。开启时若模型暂时不可用,先保留目标并等待复核;管理员创建的目标始终只标可能重复,不自动合并。清除独立模型配置只恢复沿用,不会关闭判断。`}),(0,x.jsxs)(`div`,{className:`form-grid`,children:[(0,x.jsxs)(`label`,{children:[`目标去重判断预算(秒)`,(0,x.jsx)(`input`,{type:`number`,"aria-label":`目标去重判断预算(秒)`,min:.01,max:10,step:`any`,required:!0,value:i.budget_seconds,onChange:e=>f(`budget_seconds`,e.target.value)}),(0,x.jsx)(`small`,{children:`大于 0 且不超过 10,含排队和模型调用。`})]}),(0,x.jsxs)(`label`,{children:[`目标去重判断并发`,(0,x.jsx)(`input`,{type:`number`,min:1,max:8,step:1,required:!0,value:i.concurrency,onChange:e=>f(`concurrency`,e.target.value)})]}),(0,x.jsxs)(`label`,{children:[`目标去重判断排队上限`,(0,x.jsx)(`input`,{type:`number`,min:0,max:64,step:1,required:!0,value:i.queue_limit,onChange:e=>f(`queue_limit`,e.target.value)})]})]}),s&&(0,x.jsx)(A,{error:!0,children:s}),u&&(0,x.jsx)(A,{children:`目标去重判断设置已保存`}),(0,x.jsxs)(`div`,{className:`actions`,children:[(0,x.jsx)(`button`,{className:`primary`,children:`保存目标去重判断设置`}),(0,x.jsx)(`button`,{type:`button`,onClick:()=>{o.current=!1,a(r(e)),c(``),d(!1)},children:`还原目标判断设置`})]})]})]})}function U(){return(0,x.jsxs)(`details`,{className:`persona-guide panel`,children:[(0,x.jsx)(`summary`,{children:`persona 从哪里来?与当前状态、目标有什么区别?`}),(0,x.jsxs)(`figure`,{children:[(0,x.jsx)(`figcaption`,{children:`经历成为自我认知,经过提炼与检查后形成 persona。`}),(0,x.jsxs)(`ol`,{className:`persona-flow`,children:[(0,x.jsx)(`li`,{children:`初始背景 + 持续经历`}),(0,x.jsxs)(`li`,{children:[`自我认知`,(0,x.jsx)(`small`,{children:`有来源的自我记忆`})]}),(0,x.jsxs)(`li`,{children:[`结合生成目标 + 监管要求`,(0,x.jsx)(`small`,{children:`由管理员设定,不从记忆中改写`})]}),(0,x.jsxs)(`li`,{children:[`梦境整理中提炼与检查`,(0,x.jsx)(`small`,{children:`逐句依据与表达检查`})]}),(0,x.jsxs)(`li`,{children:[`persona`,(0,x.jsx)(`small`,{children:`按发布方式发布或等待确认`})]})]})]}),(0,x.jsxs)(`dl`,{className:`persona-distinction`,children:[(0,x.jsxs)(`div`,{children:[(0,x.jsx)(`dt`,{children:`当前状态`}),(0,x.jsx)(`dd`,{children:`现在在做什么、心情如何,由宿主报告,可能过时。`})]}),(0,x.jsxs)(`div`,{children:[(0,x.jsx)(`dt`,{children:`目标与询问`}),(0,x.jsx)(`dd`,{children:`未来要做或要问的事,跨入口共享;提醒被取走不代表目标完成。`})]}),(0,x.jsxs)(`div`,{children:[(0,x.jsx)(`dt`,{children:`persona`}),(0,x.jsx)(`dd`,{children:`相对稳定的自我表达,有版本与依据;不是当前心情,也不是待办清单。`})]})]}),(0,x.jsx)(`p`,{children:`宿主结合当前状态、目标、persona、相关记忆和本入口近期消息,组织上下文并决定说什么、做什么。`}),(0,x.jsx)(`p`,{className:`muted`,children:`“待更新”表示当前版本有失效依据;“待确认”表示已有候选等待管理员决定。两者可以同时出现。`})]})}function ge({value:e,busy:t,save:n}){let[r,i]=(0,l.useState)(e),[a,o]=(0,l.useState)(``),s=(0,l.useRef)(!1);return(0,l.useEffect)(()=>{s.current||i(e)},[e]),(0,x.jsxs)(`section`,{className:`panel settings-panel persona-settings`,children:[(0,x.jsx)(`h2`,{children:`persona 的生成与发布`}),(0,x.jsx)(`p`,{children:`生成目标和监管要求只能由管理员修改,学习和整理不能修改它们。保存只影响之后接受的任务;已接受的任务、已有候选和当前 persona 保持原设置。`}),(0,x.jsxs)(`form`,{noValidate:!0,onSubmit:async e=>{if(e.preventDefault(),t)return;let a=r.goal.trim(),c=r.rules.trim();if(!a||[...a].length>4e3){o(`生成目标须为 1—4000 字,不能只含空白`);return}if(!c||[...c].length>16e3){o(`监管要求须为 1—16000 字,不能只含空白`);return}o(``);let l=await n({goal:a,rules:c,publish_mode:r.publish_mode});l&&(s.current=!1,i(l))},children:[(0,x.jsxs)(`label`,{children:[`persona 发布方式`,(0,x.jsxs)(`select`,{disabled:t,value:r.publish_mode,onChange:e=>{s.current=!0,i({...r,publish_mode:e.target.value})},children:[(0,x.jsx)(`option`,{value:`small_medium_auto`,children:`小或中自动发布`}),(0,x.jsx)(`option`,{value:`all_auto`,children:`全部自动`}),(0,x.jsx)(`option`,{value:`all_manual`,children:`全部人工确认(默认)`})]})]}),(0,x.jsx)(`p`,{className:`lifecycle-help`,children:`M3 的 persona 门槛尚未达到,默认采用“全部人工确认”:检查通过的候选也先由管理员确认,确认后才生效。可以改回“小或中自动发布”或“全部自动”。`}),(0,x.jsx)(`p`,{className:`lifecycle-help`,children:`所有候选都要经过检查。“小或中自动发布”会自动发布检查通过的小、中变化,大变化等待确认。`}),(0,x.jsx)(A,{children:`自动候选删去或改动手写内容时按“大变化”处理。选择“全部自动”后,检查通过的大变化也会直接发布,包括删改手写内容;不会等待确认。`}),(0,x.jsxs)(`label`,{children:[`生成目标`,(0,x.jsx)(`textarea`,{rows:4,disabled:t,value:r.goal,onChange:e=>{s.current=!0,i({...r,goal:e.target.value})}})]}),(0,x.jsxs)(`label`,{children:[`监管要求`,(0,x.jsx)(`textarea`,{rows:6,disabled:t,value:r.rules,onChange:e=>{s.current=!0,i({...r,rules:e.target.value})}})]}),a&&(0,x.jsx)(A,{error:!0,children:a}),(0,x.jsx)(`button`,{className:`primary`,disabled:t,children:`保存 persona 设置`})]}),(0,x.jsx)(`p`,{children:(0,x.jsx)(`a`,{href:`#/persona`,children:`查看 persona 的依据、候选和版本历史`})}),(0,x.jsx)(U,{})]})}var _e=[[`forget_threshold`,`遗忘阈值 F`,1,99],[`restore_threshold`,`恢复阈值 H`,2,100],[`feedback_increment`,`实际使用增幅`,0,100],[`confirmation_increment`,`再次确认增幅`,0,100],[`decay_amount`,`每次衰减幅度`,0,100],[`dependency_penalty`,`依据失效扣减`,0,100],[`auto_delete_days`,`遗忘后自动删除天数`,1,36500],[`upcoming_delete_days`,`即将删除窗口(天)`,1,36500],[`message_retention_days`,`消息保留天数`,1,36500]];function ve(e){let t=Object.fromEntries(_e.map(([t])=>[t,String(e[t])]));return{...e,...t,abandoned_retry_days:e.abandoned_retry_days.join(`,`)}}function ye(e){let t=e.split(/[,,]/).map(e=>e.trim()),n=t.map(Number);return n.length<=10&&t.every(Boolean)&&n.every(e=>Number.isInteger(e)&&e>=1&&e<=365)&&n.every((e,t)=>t===0||e>n[t-1])?n:null}function be({value:e,timezone:t,managedTime:n=!1}){let[r,i]=(0,l.useState)(()=>ve(e)),a=(0,l.useRef)(!1),[o,s]=(0,l.useState)(!1),[c,u]=(0,l.useState)(``),[d,f]=(0,l.useState)(!1);(0,l.useEffect)(()=>{a.current||i(ve(e))},[e]);function p(e,t){a.current=!0,f(!1),i(n=>({...n,[e]:t}))}async function m(e){if(e.preventDefault(),o)return;u(``),f(!1);for(let[e,t,n,i]of _e){let a=Number(r[e]);if(!r[e].trim()||!Number.isInteger(a)||ai){u(`${t}须为 ${n}—${i} 的整数`);return}}if(Number(r.restore_threshold)<=Number(r.forget_threshold)){u(`恢复阈值 H 必须大于遗忘阈值 F`);return}if(!n&&!/^(?:[01][0-9]|2[0-3]):[0-5][0-9]$/.test(r.maintenance_time)){u(`维护时间须为 HH:MM 格式,范围 00:00—23:59`);return}let t=ye(r.abandoned_retry_days);if(!t){u(`自动重试日须为严格递增的 1—365 整数,用逗号分隔,最多 10 个`);return}let{maintenance_time:c,...l}={...r,...Object.fromEntries(_e.map(([e])=>[e,Number(r[e])])),abandoned_retry_days:t};s(!0);try{let e=await g(`/settings/lifecycle`,_(`PATCH`,n?l:{...l,maintenance_time:c}));i(ve(e.lifecycle)),a.current=!1,f(!0)}catch(e){u(v(e))}finally{s(!1)}}return(0,x.jsxs)(`section`,{className:`panel settings-panel full-width`,children:[(0,x.jsx)(`h2`,{children:`记忆生命周期`}),(0,x.jsx)(`p`,{className:`lifecycle-help`,children:`有效记忆低于 F 时遗忘,遗忘记忆达到 H 时恢复;两阈值之间保持原状态。置顶不自动恢复。`}),n&&(0,x.jsxs)(`p`,{className:`lifecycle-help`,children:[`每日运行时间 `,e.maintenance_time,`(`,t||`角色时区`,`),在梦境整理设置中修改。`]}),(0,x.jsx)(`form`,{onSubmit:m,noValidate:!0,children:(0,x.jsxs)(`fieldset`,{disabled:o,className:`lifecycle-fieldset`,children:[(0,x.jsxs)(`div`,{className:`lifecycle-settings-grid`,children:[_e.map(([e,t,n,i])=>(0,x.jsxs)(`label`,{children:[t,(0,x.jsx)(`input`,{"aria-label":t,type:`number`,required:!0,min:n,max:i,step:1,value:r[e],onChange:t=>p(e,t.target.value)}),(0,x.jsxs)(`small`,{children:[n,`—`,i,`,整数`]})]},e)),!n&&(0,x.jsxs)(`label`,{children:[`维护时间`,(0,x.jsx)(`input`,{"aria-label":`维护时间`,required:!0,type:`text`,inputMode:`text`,placeholder:`03:00`,maxLength:5,value:r.maintenance_time,onChange:e=>p(`maintenance_time`,e.target.value)}),(0,x.jsxs)(`small`,{children:[`HH:MM · 角色时区 `,t||`未设置`]})]})]}),(0,x.jsxs)(`label`,{className:`check-label`,children:[(0,x.jsx)(`input`,{type:`checkbox`,checked:r.auto_delete_enabled,onChange:e=>p(`auto_delete_enabled`,e.target.checked)}),`自动删除遗忘记忆`]}),(0,x.jsxs)(`label`,{className:`check-label`,children:[(0,x.jsx)(`input`,{type:`checkbox`,checked:r.abandoned_retry_enabled,onChange:e=>p(`abandoned_retry_enabled`,e.target.checked)}),`自动重试已放弃批次`]}),(0,x.jsxs)(`label`,{children:[`自动重试日`,(0,x.jsx)(`input`,{"aria-label":`自动重试日`,type:`text`,placeholder:`1,3,7`,value:r.abandoned_retry_days,onChange:e=>p(`abandoned_retry_days`,e.target.value)}),(0,x.jsx)(`small`,{children:`放弃后第几天重试,逗号分隔`})]}),(0,x.jsx)(`p`,{className:`lifecycle-help`,children:`维护按实际执行次数衰减,遗忘和置顶记忆不衰减。依据失效时每个依据只扣一次,置顶期间暂缓扣减。已放弃的批次按上面的日期(角色时区)各自动重试一次,每次使用完整尝试额度;停机错过的日期恢复后最多补一次。用完后缺口只作记录,仍可手动重新学习,手动重新学习不占自动重试次数。内容拒绝不自动重试。`}),(0,x.jsx)(`p`,{className:`lifecycle-help`,children:`关闭自动删除后仍保留天数设置,“即将删除”列表为空。消息只有超过保留期、已结束学习且没有受保护的引用时才清理。新设置用于以后接受的维护,当前运行沿用接受时的设置。`}),c&&(0,x.jsx)(A,{error:!0,children:c}),d&&(0,x.jsx)(A,{children:`生命周期设置已保存`}),(0,x.jsxs)(`div`,{className:`actions`,children:[(0,x.jsx)(`button`,{className:`primary`,children:o?`正在保存…`:`保存生命周期设置`}),(0,x.jsx)(`button`,{type:`button`,className:`secondary`,onClick:()=>{i(ve(e)),a.current=!1,u(``),f(!1)},children:`还原为最近读取的设置`})]})]})})]})}var xe={person_summary_settings:`更新人物摘要设置`,person_summary_edit:`编辑发布人物摘要`,person_summary_confirm:`确认人物摘要候选`,person_summary_reject:`拒绝人物摘要候选`,person_summary_rollback:`回滚人物摘要`,person_summary_regenerate_requested:`请求重新生成人物摘要`,person_summary_generation_requested:`请求生成人物摘要`,person_summary_generation_published:`人物摘要生成并发布`,person_summary_generation_candidate:`人物摘要待确认`,person_summary_generation_rejected:`人物摘要检查未通过`,person_summary_generation_skipped:`跳过人物摘要生成`,person_summary_generation_interrupted:`人物摘要生成中断`,token_create:`创建宿主令牌`,token_revoke:`撤销宿主令牌`,token_limits_saved:`更新宿主令牌限流`,backup_export:`导出备份`,backup_import:`从备份导入`,consolidation_settings_saved:`更新梦境整理设置`,goal_dedup_judge_saved:`更新目标去重判断设置`,consolidation_merge:`整理合并记忆`,consolidation_conflict:`整理提出矛盾建议`,consolidation_dependency:`整理提出依赖复核建议`,consolidation_annotation_confirm:`确认整理建议`,consolidation_annotation_clear:`清除整理建议`,goal_basis_review:`目标依据复核`,goal_basis_clear:`清除目标依据标注`,goal_auto_review:`每日维护自动关闭目标`,goal_reopen:`重开目标`,goal_duplicate_auto_judged:`每日维护补判可能重复`,goal_merge_undo:`撤销自动合并`,persona_settings_saved:`更新 persona 设置`,persona_edit:`编辑发布 persona`,persona_confirm:`确认 persona 候选`,persona_reject:`拒绝 persona 候选`,persona_rollback:`回滚 persona`,persona_current:`发布 persona`,persona_pending:`persona 候选待确认`,persona_rejected:`persona 候选被拒绝`,persona_generation_requested:`请求生成 persona`,persona_generation_current:`persona 生成并发布`,persona_generation_pending:`persona 生成后待确认`,persona_generation_rejected:`persona 生成检查未通过`,persona_generation_skipped:`跳过 persona 生成`,persona_generation_conflict:`persona 生成遇到版本或依据冲突`,persona_generation_failed:`persona 生成失败`,persona_generation_interrupted:`persona 生成中断`,goal_dedup_judged:`目标去重判断结果`,goal_dedup_queued:`目标待去重复核`,goal_create:`创建目标`,goal_update:`更新目标`,goal_merge:`合并重复目标`,goal_duplicate_dismiss:`驳回目标重复`,settings_goals:`更新目标与提醒设置`,memory_adjust:`调整记忆`,memory_forget:`手动遗忘`,memory_bulk_forget:`批量遗忘记忆`,memory_bulk_delete:`批量删除记忆`,memory_bulk_purge:`批量彻底清除记忆`,memory_restore:`恢复记忆`,memory_purge:`彻底清除记忆`,memory_recreate:`按旧内容新建`,memory_revision:`人工修订记忆`,lifecycle_saved:`更新生命周期设置`,state_settings_saved:`更新当前状态设置`,maintenance_requested:`请求维护`,maintenance_completed:`维护完成`,batch_relearn:`重新学习批次`,batch_result:`批次结果`,learn_requested:`请求立即学习`,feedback:`使用反馈`,feedback_rejected:`使用反馈被拒绝`,learn_rejected:`立即学习请求被拒绝`,trial_media_uploaded:`上传试用图片`,trial_entry_created:`创建试用入口`,trial_speaker_created:`创建发言人`,trial_message_received:`试用消息接收`,trial_prepare:`试用回复准备`,trial_reply:`试用角色回复`,setup_password:`设置管理员密码`,setup_completed:`完成首次设置`,models_saved:`更新模型配置`,role_saved:`更新角色设置`,limits_saved:`更新用量与并发`,model_retry:`请求模型重试`,model_test:`测试模型连接`,recall_judge_saved:`更新召回判断设置`,entry_visibility:`更新入口记忆可见范围`,entry_settings_updated:`更新入口节奏与过滤`,subject_alias_added:`添加人物别名`,subject_alias_removed:`删除人物别名`,subject_link_denied:`否认人物联系`,subjects_merged:`确认并合并人物`,models_imported:`导入模型配置`,login:`管理员登录`,logout:`退出登录`},Se={person_summary:`人物摘要`,local_cli:`本机命令行`,persona:`persona 更新`,consolidation:`梦境整理`,goal_dedup:`目标去重判断`,learning:`学习`,admin:`管理员`,host:`宿主`,system:`系统`,scheduler:`后台调度`,maintenance:`维护`,local_import:`本机导入`},Ce={person_summary:`人物摘要`,host_token:`宿主令牌`,backup:`备份`,persona:`persona 版本`,persona_attempt:`persona 生成任务`,goal:`目标`,memory:`记忆`,memory_bulk:`批量记忆操作`,batch:`批次`,entry:`入口`,subject:`人物`,subject_link:`人物联系`,model:`模型`,settings:`设置`,maintenance:`维护`,recall:`召回`,request:`请求`};function we({initialQuery:e=``}){let t=new URLSearchParams(e),[n,r]=(0,l.useState)({time_from:``,time_to:``,action:``,actor:``,object_type:t.get(`object_type`)||``,object_id:t.get(`object_id`)||``}),i=e=>new URLSearchParams(Object.entries(e).filter(([,e])=>!!e)).toString(),[a,o]=(0,l.useState)(()=>i(n)),[s,c]=(0,l.useState)(0),[u,d]=(0,l.useState)(``),f=y(`/operations?${a}&limit=30&offset=${s}`),p=(e,t)=>r(n=>({...n,[e]:t}));function m(e){if(e.preventDefault(),n.time_from&&n.time_to&&n.time_from>n.time_to){d(`开始日期不能晚于结束日期`);return}d(``),c(0),o(i(n)),f.refresh()}return(0,x.jsxs)(x.Fragment,{children:[(0,x.jsx)(`div`,{className:`page-heading`,children:(0,x.jsxs)(`div`,{children:[(0,x.jsx)(`p`,{className:`eyebrow`,children:`每次变更,均有记录`}),(0,x.jsx)(`h1`,{children:`操作记录`}),(0,x.jsx)(`p`,{children:`查看管理员、宿主与系统保存的操作摘要。`})]})}),(0,x.jsx)(`section`,{className:`panel`,children:(0,x.jsxs)(`form`,{onSubmit:m,className:`memory-filters`,children:[(0,x.jsxs)(`div`,{className:`filter-grid operation-filters`,children:[(0,x.jsxs)(`label`,{children:[`开始日期`,(0,x.jsx)(`input`,{type:`date`,value:n.time_from,onChange:e=>p(`time_from`,e.target.value)})]}),(0,x.jsxs)(`label`,{children:[`结束日期`,(0,x.jsx)(`input`,{type:`date`,value:n.time_to,onChange:e=>p(`time_to`,e.target.value)})]}),(0,x.jsxs)(`label`,{children:[`操作类型`,(0,x.jsxs)(`select`,{value:n.action,onChange:e=>p(`action`,e.target.value),children:[(0,x.jsx)(`option`,{value:``,children:`全部类型`}),Object.entries(xe).map(([e,t])=>(0,x.jsx)(`option`,{value:e,children:t},e))]})]}),(0,x.jsxs)(`label`,{children:[`操作者`,(0,x.jsxs)(`select`,{value:n.actor,onChange:e=>p(`actor`,e.target.value),children:[(0,x.jsx)(`option`,{value:``,children:`全部操作者`}),Object.entries(Se).map(([e,t])=>(0,x.jsx)(`option`,{value:e,children:t},e))]})]}),(0,x.jsxs)(`label`,{children:[`对象类型`,(0,x.jsxs)(`select`,{value:n.object_type,onChange:e=>p(`object_type`,e.target.value),children:[(0,x.jsx)(`option`,{value:``,children:`全部对象`}),Object.entries(Ce).map(([e,t])=>(0,x.jsx)(`option`,{value:e,children:t},e))]})]}),(0,x.jsxs)(`label`,{children:[`对象标识`,(0,x.jsx)(`input`,{value:n.object_id,maxLength:200,placeholder:`如记忆编号 8`,onChange:e=>p(`object_id`,e.target.value)})]})]}),(0,x.jsx)(`p`,{className:`fine-print`,children:`日期筛选按角色时区解释,结束日期包含当天;记录时间按本机时区显示。`}),(0,x.jsxs)(`div`,{className:`actions`,children:[(0,x.jsx)(`button`,{className:`primary`,children:`筛选记录`}),(0,x.jsx)(`button`,{type:`button`,className:`secondary`,onClick:f.refresh,children:`刷新记录`})]})]})}),(u||f.error)&&(0,x.jsx)(A,{error:!0,children:u||f.error}),(0,x.jsx)(`div`,{className:`list-heading`,children:(0,x.jsx)(`span`,{children:f.data?`共 ${f.data.total} 条`:`正在读取…`})}),f.data?.items.length===0&&(0,x.jsx)(j,{title:`没有符合条件的操作记录`}),(0,x.jsx)(`div`,{className:`learning-list`,children:f.data?.items.map(e=>(0,x.jsxs)(`article`,{className:`panel operation-card`,children:[(0,x.jsxs)(`div`,{className:`panel-heading`,children:[(0,x.jsx)(`h2`,{children:xe[e.action]||e.action}),(0,x.jsxs)(`span`,{className:`muted`,children:[`记录 #`,e.id]})]}),(0,x.jsxs)(`p`,{children:[w(e.created_at),` · `,Se[e.actor]||e.actor]}),e.object_type&&(0,x.jsxs)(`p`,{children:[`对象:`,Ce[e.object_type]||e.object_type,e.object_id===null?``:` · ${e.object_id}`]}),(0,x.jsxs)(`details`,{children:[(0,x.jsx)(`summary`,{children:`查看记录详情`}),(0,x.jsx)(`pre`,{className:`record-json`,children:JSON.stringify(e.details,null,2)})]})]},e.id))}),f.data&&(0,x.jsx)(T,{total:f.data.total,offset:s,change:c})]})}var Te=[`chat`,`embedding`,`recall_judge`,`goal_dedup_judge`,`image_understanding`],Ee={chat:`对话模型`,embedding:`Embedding 模型`,recall_judge:`召回判断模型`,goal_dedup_judge:`目标去重判断模型`,image_understanding:`图片理解模型`},De=()=>Intl.DateTimeFormat().resolvedOptions().timeZone||`Asia/Shanghai`,Oe=e=>e.inherited?{enabled:!1}:{enabled:e.enabled,base_url:e.base_url,model:e.model,dimensions:e.dimensions,reasoning_effort:e.reasoning_effort,...e.vision===void 0?{}:{vision:e.vision},...e.clearKey?{api_key:``}:e.key?{api_key:e.key}:{}};function ke({value:e,change:t}){return(0,x.jsxs)(`div`,{className:`form-grid`,children:[(0,x.jsxs)(`label`,{children:[`角色名`,(0,x.jsx)(`input`,{required:!0,maxLength:100,value:e.name,onChange:n=>t({...e,name:n.target.value})})]}),(0,x.jsxs)(`label`,{children:[`时区`,(0,x.jsx)(`input`,{required:!0,value:e.timezone||``,placeholder:`Asia/Shanghai`,onChange:n=>t({...e,timezone:n.target.value})})]}),(0,x.jsxs)(`label`,{className:`full-width`,children:[`背景材料(可选)`,(0,x.jsx)(`textarea`,{maxLength:16e3,rows:4,value:e.background,onChange:n=>t({...e,background:n.target.value})}),(0,x.jsx)(`small`,{children:`保存为“设定”的自我记忆,不会当作亲身经历。`})]})]})}function Ae({kind:e,value:t,change:n,settings:r}){let i=Ee[e],a=r.model_source===`external`,o=r.presets.find(e=>e.id===`ark-glm`)?.reasoning_effort,[s,c]=(0,l.useState)(``),[u,d]=(0,l.useState)(!1),f=e===`recall_judge`||e===`goal_dedup_judge`,p=e===`image_understanding`,m=(f||p)&&t.inherited,h=m&&!r.models[e].inherited,y=m?r.models.chat.enabled&&!h:t.enabled,b=async()=>{d(!0),c(``);try{let n=await g(`/settings/models/${e}/test`,_(`POST`,a||m?{}:Oe(t)));c(`${n.message} · ${n.duration_ms} ms`)}catch(e){c(v(e))}finally{d(!1)}};return(0,x.jsxs)(`div`,{className:`model-fields`,children:[f||p?(0,x.jsxs)(`label`,{className:`check-label`,children:[(0,x.jsx)(`input`,{type:`checkbox`,disabled:a,checked:p?t.enabled&&!t.inherited:!t.inherited,onChange:e=>{c(``),n({...t,inherited:!e.target.checked&&(!p||!!r.models.chat.vision),enabled:!p||e.target.checked})}}),`单独配置`,i]}):(0,x.jsxs)(`label`,{className:`check-label`,children:[(0,x.jsx)(`input`,{type:`checkbox`,disabled:a,checked:t.enabled,onChange:e=>n({...t,enabled:e.target.checked})}),`启用`,i]}),m?(0,x.jsxs)(`p`,{children:[p?`沿用已保存的对话模型接口、模型、密钥和推理档位。当前模型:`:`沿用已保存的对话模型接口、模型和密钥,推理档位 high。当前模型:`,r.models.chat.model||`未配置`,`。`]}):(0,x.jsxs)(`fieldset`,{disabled:a||!t.enabled,children:[e!==`embedding`&&e!==`image_understanding`&&(0,x.jsxs)(`label`,{children:[i,`服务商预设`,(0,x.jsxs)(`select`,{defaultValue:``,onChange:e=>{let i=r.presets.find(t=>t.id===e.target.value);i&&n({...t,base_url:i.base_url,model:i.model,reasoning_effort:i.reasoning_effort})},children:[(0,x.jsx)(`option`,{value:``,children:`自定义 OpenAI 兼容服务`}),r.presets.map(e=>(0,x.jsx)(`option`,{value:e.id,children:e.name},e.id))]})]}),(0,x.jsxs)(`label`,{children:[i,`接口地址`,(0,x.jsx)(`input`,{type:`url`,required:t.enabled,value:t.base_url,placeholder:`https://…/v1`,onChange:e=>n({...t,base_url:e.target.value})})]}),(0,x.jsxs)(`label`,{children:[i,`模型名`,(0,x.jsx)(`input`,{required:t.enabled,value:t.model,maxLength:200,onChange:e=>n({...t,model:e.target.value})})]}),(0,x.jsxs)(`label`,{children:[i,` API key`,(0,x.jsx)(`input`,{"aria-label":`${i} API key`,type:`password`,autoComplete:`new-password`,value:t.key||``,placeholder:t.key_set?`已设置;留空保留`:`未设置(本地服务可留空)`,onChange:e=>n({...t,key:e.target.value,clearKey:!1})}),(0,x.jsxs)(`small`,{children:[`密钥`,t.key_set?`已设置`:`未设置`,`,保存后不回显。`]})]}),t.key_set&&(0,x.jsxs)(`label`,{className:`check-label`,children:[(0,x.jsx)(`input`,{type:`checkbox`,checked:t.clearKey||!1,onChange:e=>n({...t,clearKey:e.target.checked,key:``})}),`移除已保存的密钥`]}),e===`chat`&&(0,x.jsxs)(`label`,{className:`check-label`,children:[(0,x.jsx)(`input`,{type:`checkbox`,checked:!!t.vision,onChange:e=>n({...t,vision:e.target.checked})}),`支持图片输入(vision)`,(0,x.jsx)(`small`,{children:`对话模型支持图片输入。开启后,图片理解未单独配置时沿用对话模型。学习时是否直接看原图,请在“用量与学习”中设置。`})]}),e===`embedding`?(0,x.jsxs)(`label`,{children:[i,`向量维度`,(0,x.jsx)(`input`,{type:`number`,min:1,max:65536,value:t.dimensions??``,onChange:e=>n({...t,dimensions:e.target.value?Number(e.target.value):null})}),(0,x.jsx)(`small`,{children:`可选;更换模型或维度后须重新标定召回。`})]}):(0,x.jsxs)(`label`,{children:[i,`推理档位`,(0,x.jsx)(`input`,{"aria-label":`${i}推理档位`,value:t.reasoning_effort||``,maxLength:50,placeholder:f?`留空使用 high`:`可选;留空不发送`,onChange:e=>n({...t,reasoning_effort:e.target.value||null})}),o&&e!==`image_understanding`&&(0,x.jsxs)(`small`,{children:[`火山方舟 GLM 预设使用 `,o,`。`]}),f&&(0,x.jsx)(`small`,{children:`判断档位留空时,后端使用 high。`})]})]}),(0,x.jsxs)(`div`,{className:`button-row`,children:[(0,x.jsx)(`button`,{type:`button`,disabled:u||!y,onClick:()=>void b(),children:u?`正在测试…`:`测试${i}连接`}),s&&(0,x.jsx)(`span`,{role:`status`,children:s})]}),h&&(0,x.jsx)(`p`,{className:`muted`,children:`保存沿用设置后可测试连接。`})]})}function je({done:e}){let t=y(`/settings`);return t.error?(0,x.jsxs)(A,{error:!0,children:[t.error,(0,x.jsx)(`button`,{onClick:t.refresh,children:`重新加载`})]}):t.data?(0,x.jsx)(Me,{settings:t.data,done:e}):(0,x.jsx)(`p`,{role:`status`,children:`正在读取设置…`})}function Me({settings:e,done:t}){let[n,r]=(0,l.useState)({...e.role,timezone:e.role.timezone||De()}),[i,a]=(0,l.useState)(e.models),[o,s]=(0,l.useState)(!1),[c,u]=(0,l.useState)(``);return(0,x.jsxs)(`form`,{onSubmit:async r=>{r.preventDefault(),s(!0),u(``);try{if(e.model_source===`local`)for(let t of[`chat`,`embedding`])(i[t].enabled||e.models[t].enabled)&&await g(`/settings/models/${t}`,_(`PUT`,Oe(i[t])));await g(`/setup/complete`,_(`POST`,n)),history.replaceState(null,``,`/#/trial`),t()}catch(e){u(v(e))}finally{s(!1)}},className:`setup-form`,children:[(0,x.jsxs)(`section`,{children:[(0,x.jsx)(`h2`,{children:`2 · 认识角色`}),(0,x.jsx)(ke,{value:n,change:e=>r({...e,timezone:e.timezone||De()})})]}),(0,x.jsxs)(`section`,{children:[(0,x.jsx)(`h2`,{children:`3 · 配置模型`}),(0,x.jsx)(`p`,{children:`对话模型用于学习和试用回复,也可以稍后再配。Embedding 可选,未配置时使用全文检索。`}),e.model_source===`external`&&(0,x.jsx)(A,{children:`配置来自外部文件,只读;请在外部文件中修改。`}),[`chat`,`embedding`].map(t=>(0,x.jsxs)(`div`,{className:`setup-model`,children:[(0,x.jsx)(`h3`,{children:t===`chat`?`对话模型`:`Embedding 模型(可选)`}),(0,x.jsx)(Ae,{kind:t,value:i[t],change:e=>a({...i,[t]:e}),settings:e})]},t))]}),!i.chat.enabled&&(0,x.jsx)(A,{children:`未配置模型,暂不学习;完成设置后仍可接收消息。`}),c&&(0,x.jsx)(A,{error:!0,children:c}),(0,x.jsx)(`button`,{className:`primary`,disabled:o,children:o?`正在保存…`:`完成设置并开始试用`})]})}function Ne(){let e=y(`/settings`,2e3);return(0,x.jsxs)(x.Fragment,{children:[(0,x.jsx)(`div`,{className:`page-heading`,children:(0,x.jsxs)(`div`,{children:[(0,x.jsx)(`span`,{className:`eyebrow`,children:`管理`}),(0,x.jsx)(`h1`,{children:`设置`}),(0,x.jsx)(`p`,{children:`保存后对新的工作生效。`})]})}),e.error&&(0,x.jsx)(A,{error:!0,children:e.error}),e.data?(0,x.jsx)(Pe,{settings:e.data}):(0,x.jsx)(`p`,{children:`正在读取设置…`})]})}function Pe({settings:e}){let[t,n]=(0,l.useState)(e);(0,l.useEffect)(()=>n(e),[e]);let[r,i]=(0,l.useState)({...t.role,timezone:t.role.timezone||De()}),[a,o]=(0,l.useState)(t.models),[s,c]=(0,l.useState)(``),[u,d]=(0,l.useState)(``),[f,p]=(0,l.useState)(``),[m,h]=(0,l.useState)(!1),[y,b]=(0,l.useState)(String(t.state.stale_after_minutes)),S=(0,l.useRef)(!1),C=(0,l.useRef)(new Set);(0,l.useEffect)(()=>{S.current||b(String(t.state.stale_after_minutes))},[t.state.stale_after_minutes]);let w=async(e,t,r)=>{if(!m){h(!0),d(``),p(``);try{let i=await g(e,_(t,r));n(i),e===`/settings/state`&&(S.current=!1,b(String(i.state.stale_after_minutes)));for(let t of Te)e===`/settings/models/${t}`&&(C.current.delete(t),o(e=>Object.fromEntries(Te.map(n=>[n,n===t||!C.current.has(n)?i.models[n]:e[n]]))));return p(`已保存`),i}catch(t){return c(e),d(v(t)),null}finally{h(!1)}}};return(0,x.jsxs)(`div`,{className:`settings-grid`,children:[u&&![`/settings/limits`,`/settings/goals`,`/settings/consolidation`].includes(s)&&(0,x.jsx)(A,{error:!0,children:u}),f&&(0,x.jsx)(A,{children:f}),(0,x.jsxs)(`section`,{className:`panel settings-panel`,children:[(0,x.jsx)(`h2`,{children:`角色`}),(0,x.jsxs)(`form`,{onSubmit:e=>{e.preventDefault(),w(`/settings/role`,`PATCH`,r)},children:[(0,x.jsx)(ke,{value:r,change:e=>i({...e,timezone:e.timezone||De()})}),(0,x.jsx)(`button`,{disabled:m,className:`primary`,children:`保存角色`})]})]}),t.model_source===`external`&&(0,x.jsx)(A,{children:`配置来自外部文件,只读;数据库模型配置和 secrets.json 不用于本次服务。`}),Te.filter(e=>t.models[e]).map(e=>(0,x.jsxs)(`section`,{className:`panel settings-panel`,"aria-label":Ee[e],children:[(0,x.jsx)(`h2`,{children:Ee[e]}),(0,x.jsxs)(`p`,{children:[`当前状态:`,e===`image_understanding`&&!t.models[e].enabled?`未启用`:D(t.health[e]?.state||`configuration_error`)]}),e===`image_understanding`&&(0,x.jsxs)(`p`,{children:[t.models[e].inherited?`当前图片理解沿用对话模型。`:t.models[e].enabled?`当前图片理解使用独立配置。`:`当前图片理解未启用。`,`未单独配置且对话模型开启 vision 时会自动沿用;取消独立配置会恢复此规则。 测试连接会发送一张固定测试图片,健康状态与用量仍按图片理解用途记录。`]}),(0,x.jsxs)(`form`,{onSubmit:t=>{t.preventDefault(),w(`/settings/models/${e}`,`PUT`,Oe(a[e]))},children:[(0,x.jsx)(Ae,{kind:e,value:a[e],change:t=>{C.current.add(e),o({...a,[e]:t})},settings:t}),t.model_source===`local`&&(0,x.jsxs)(`button`,{className:`primary`,disabled:m,children:[`保存`,Ee[e]]})]}),[`temporarily_unavailable`,`account_problem`].includes(t.health[e]?.state)&&(0,x.jsx)(`button`,{onClick:()=>void w(`/settings/models/${e}/retry`,`POST`,{}),children:`立即重试`}),e===`goal_dedup_judge`&&t.goal_dedup_judge&&(0,x.jsx)(H,{value:t.goal_dedup_judge,busy:m,save:async e=>(await w(`/settings/goal-dedup-judge`,`PATCH`,e))?.goal_dedup_judge||null}),e===`recall_judge`&&(0,x.jsx)(Fe,{value:t.recall_judge})]},e)),(0,x.jsx)(I,{value:t,busy:m,saveError:s===`/settings/limits`?u:``,nativeImagesAvailable:t.models.chat.enabled&&t.models.chat.vision===!0,chatEnabled:t.models.chat.enabled,external:t.model_source===`external`,save:e=>w(`/settings/limits`,`PATCH`,e)}),(0,x.jsxs)(`section`,{className:`panel settings-panel`,children:[(0,x.jsx)(`h2`,{children:`当前状态`}),(0,x.jsx)(`p`,{className:`lifecycle-help`,children:`宿主报告的活动与情绪供当前回复使用。超过设定分钟数未更新时,只标注“可能过时”,不会自动结束活动。`}),(0,x.jsxs)(`form`,{noValidate:!0,onSubmit:e=>{if(e.preventDefault(),m)return;let t=Number(y);if(!y.trim()||!Number.isInteger(t)||t<1||t>525600){p(``),c(`/settings/state`),d(`可能过时的分钟数须为 1—525600 的整数`);return}w(`/settings/state`,`PATCH`,{stale_after_minutes:t})},children:[(0,x.jsxs)(`label`,{children:[`可能过时的分钟数`,(0,x.jsx)(`input`,{"aria-label":`可能过时的分钟数`,type:`number`,required:!0,min:1,max:525600,step:1,disabled:m,value:y,onChange:e=>{S.current=!0,b(e.target.value),p(``)}}),(0,x.jsx)(`small`,{children:`1—525600 的整数,默认 30 分钟;严格超过此时长才标记。`})]}),(0,x.jsxs)(`p`,{className:`lifecycle-help`,children:[`保存后立即影响后续状态读取。`,(0,x.jsx)(`a`,{href:`#/state`,children:`查看当前状态`})]}),(0,x.jsx)(`button`,{className:`primary`,disabled:m,children:`保存当前状态设置`})]})]}),(0,x.jsx)(te,{value:t.goals,busy:m,saveError:s===`/settings/goals`?u:``,save:async e=>(await w(`/settings/goals`,`PATCH`,e))?.goals||null}),t.consolidation&&(0,x.jsx)(he,{value:t.consolidation,saveError:s===`/settings/consolidation`?u:``,timezone:t.role.timezone,busy:m,save:async e=>(await w(`/settings/consolidation`,`PATCH`,e))?.consolidation||null}),(0,x.jsx)(ge,{value:t.persona,busy:m,save:async e=>(await w(`/settings/persona`,`PATCH`,e))?.persona||null}),(0,x.jsxs)(`section`,{className:`panel settings-panel`,children:[(0,x.jsx)(`h2`,{children:`最近操作`}),(0,x.jsx)(`a`,{className:`text-button`,href:`#/operations`,children:`查看全部操作记录`}),(0,x.jsx)(`ul`,{className:`operations`,children:t.operations.map(e=>(0,x.jsxs)(`li`,{children:[(0,x.jsx)(`span`,{children:xe[e.action]||e.action}),(0,x.jsxs)(`small`,{children:[new Date(e.created_at).toLocaleString(`zh-CN`),` ·`,` `,Se[e.actor]||e.actor]})]},e.id))})]}),(0,x.jsx)(be,{managedTime:!!t.consolidation,value:t.lifecycle,timezone:t.role.timezone})]})}function Fe({value:e}){let[t,n]=(0,l.useState)(e.enabled),[r,i]=(0,l.useState)(String(e.concurrency)),[a,o]=(0,l.useState)(String(e.queue_limit)),[s,c]=(0,l.useState)(!1),[u,d]=(0,l.useState)(``),[f,p]=(0,l.useState)(!1);async function m(e){if(e.preventDefault(),!s){if(p(!1),!r.trim()||!Number.isInteger(Number(r))||Number(r)<1||Number(r)>8){d(`召回判断并发须为 1—8 的整数`);return}if(!a.trim()||!Number.isInteger(Number(a))||Number(a)<0||Number(a)>64){d(`召回判断排队上限须为 0—64 的整数`);return}c(!0),d(``);try{await g(`/settings/recall-judge`,_(`PATCH`,{enabled:t,concurrency:Number(r),queue_limit:Number(a)})),p(!0)}catch(e){d(v(e))}finally{c(!1)}}}return(0,x.jsxs)(`form`,{className:`judge-settings`,onSubmit:m,noValidate:!0,children:[(0,x.jsx)(`h3`,{children:`判断开关与排队`}),(0,x.jsx)(`p`,{children:`每次回复准备最多判断 8 条相关记忆,总预算 10 秒(含排队)。判断失败或暂停时保留仍有效的原召回;人物要点不参与判断。`}),u&&(0,x.jsx)(A,{error:!0,children:u}),f&&(0,x.jsx)(A,{children:`召回判断设置已保存`}),(0,x.jsxs)(`fieldset`,{className:`lifecycle-fieldset`,disabled:s,children:[(0,x.jsxs)(`label`,{className:`check-label`,children:[(0,x.jsx)(`input`,{type:`checkbox`,checked:t,onChange:e=>n(e.target.checked)}),`启用召回判断`]}),(0,x.jsx)(`p`,{className:`muted`,children:`关闭后保留基础召回。清除独立模型配置只会恢复沿用对话模型,不会关闭判断。`}),(0,x.jsxs)(`div`,{className:`form-grid`,children:[(0,x.jsxs)(`label`,{children:[`召回判断并发`,(0,x.jsx)(`input`,{type:`number`,min:1,max:8,step:1,required:!0,value:r,onChange:e=>i(e.target.value)})]}),(0,x.jsxs)(`label`,{children:[`召回判断排队上限`,(0,x.jsx)(`input`,{type:`number`,min:0,max:64,step:1,required:!0,value:a,onChange:e=>o(e.target.value)})]})]})]}),(0,x.jsx)(`button`,{className:`primary`,disabled:s,children:`保存召回判断设置`})]})}function Ie({children:e}){let[t,n]=(0,l.useState)(null),[r,i]=(0,l.useState)(``),a=(0,l.useCallback)(()=>{g(`/session`).then(e=>{d(e.csrf_token),n(e),i(``)}).catch(e=>i(v(e)))},[]);return(0,l.useEffect)(()=>(a(),addEventListener(`iris-auth`,a),()=>removeEventListener(`iris-auth`,a)),[a]),t?.configured&&t.authenticated?e(a):(0,x.jsxs)(`main`,{className:`access-page`,children:[(0,x.jsxs)(`div`,{className:`access-brand`,children:[(0,x.jsx)(`span`,{"aria-hidden":!0,children:`✳`}),` Iris `,(0,x.jsx)(`small`,{children:`相处与记忆`})]}),(0,x.jsxs)(`div`,{className:`panel access-card`,children:[(0,x.jsx)(`span`,{className:`eyebrow`,children:`仅本机管理`}),(0,x.jsx)(`h1`,{children:t?.configured?`管理员登录`:`首次设置`}),r&&(0,x.jsxs)(A,{error:!0,children:[r,(0,x.jsx)(`button`,{onClick:a,children:`重新连接`})]}),t?t.authenticated?(0,x.jsx)(je,{done:a}):(0,x.jsx)(Le,{creating:!t.admin_exists,configured:t.configured,setupCodeRequired:!!t.setup_code_required,done:a}):(0,x.jsx)(`p`,{children:`正在连接…`})]})]})}function Le({creating:e,configured:t,setupCodeRequired:n,done:r}){let[i,a]=(0,l.useState)(``),[o,s]=(0,l.useState)(``),[c,u]=(0,l.useState)(``),[d,f]=(0,l.useState)(``),[p,m]=(0,l.useState)(!1);return(0,x.jsxs)(`form`,{className:`password-form`,onSubmit:async n=>{if(n.preventDefault(),e&&i!==o){f(`两次密码不一致`);return}m(!0),f(``);try{await g(e?`/setup/password`:`/login`,_(`POST`,{password:i,...e&&c?{setup_code:c}:{}})),a(``),s(``),u(``),t&&location.pathname!==`/`&&history.replaceState(null,``,`/#/trial`),r()}catch(e){f(v(e))}finally{m(!1)}},children:[(0,x.jsxs)(`p`,{children:[e?`1 · 先设置管理员密码,保护记忆与设置。`:`请输入管理员密码。`,!e&&!t&&`登录后继续完成首次设置。`]}),(0,x.jsxs)(`label`,{children:[`管理员密码`,(0,x.jsx)(`input`,{required:!0,type:`password`,"aria-label":`管理员密码`,minLength:8,maxLength:256,autoComplete:e?`new-password`:`current-password`,value:i,onChange:e=>a(e.target.value)}),(0,x.jsx)(`small`,{children:`至少 8 个字符。`})]}),e&&(0,x.jsxs)(`label`,{children:[`确认密码`,(0,x.jsx)(`input`,{required:!0,type:`password`,minLength:8,maxLength:256,autoComplete:`new-password`,value:o,onChange:e=>s(e.target.value)})]}),e&&n&&(0,x.jsxs)(`label`,{children:[`设置码`,(0,x.jsx)(`input`,{required:!0,type:`password`,"aria-label":`设置码`,maxLength:128,autoComplete:`off`,value:c,onChange:e=>u(e.target.value)}),(0,x.jsx)(`small`,{children:`从服务启动日志取得;仅首次设置使用。`})]}),d&&(0,x.jsx)(A,{error:!0,children:d}),(0,x.jsx)(`button`,{className:`primary`,disabled:p,children:p?`正在验证…`:e?`设置密码并继续`:`登录`})]})}function Re({value:e}){return e.kind===`all`?(0,x.jsx)(`span`,{children:`全部入口`}):e.kind===`prefix`?(0,x.jsxs)(`span`,{children:[`入口前缀:`,(0,x.jsx)(`code`,{children:e.prefix})]}):(0,x.jsxs)(`span`,{children:[`入口列表:`,e.entries.map((e,t)=>(0,x.jsxs)(`span`,{children:[t>0&&`、`,(0,x.jsx)(`code`,{children:e})]},e))]})}function ze(){let e=y(`/tokens`,5e3),[t,n]=(0,l.useState)(!1),[r,i]=(0,l.useState)(null),[a,o]=(0,l.useState)(!1),[s,c]=(0,l.useState)(``),[u,d]=(0,l.useState)(``);return(0,x.jsxs)(`section`,{className:`panel access-panel`,"aria-label":`宿主令牌`,children:[(0,x.jsxs)(`div`,{className:`panel-heading`,children:[(0,x.jsx)(`h2`,{children:`宿主令牌`}),(0,x.jsx)(`button`,{className:`primary`,onClick:()=>{d(``),n(!0)},children:`创建宿主令牌`})]}),(0,x.jsx)(`p`,{children:`每个宿主使用自己的令牌。入口范围控制该宿主可以操作哪些入口,不是记忆可见范围设置。`}),e.data&&(0,x.jsxs)(`p`,{className:`muted`,children:[`每令牌限流:每秒 `,e.data.rate_limits.rate_per_second,` 次,突发上限`,` `,e.data.rate_limits.burst,` 次。`]}),u&&(0,x.jsx)(A,{children:u}),e.error&&(0,x.jsxs)(A,{error:!0,children:[e.error,(0,x.jsx)(`button`,{onClick:e.refresh,children:`重试令牌列表`})]}),!e.data&&!e.error&&(0,x.jsx)(`p`,{children:`正在读取令牌…`}),e.data?.items.length===0&&(0,x.jsx)(j,{title:`尚未创建宿主令牌`,children:`创建令牌后,将它配置到同一台电脑上的宿主程序。`}),(0,x.jsx)(`div`,{className:`access-records`,children:e.data?.items.map(e=>(0,x.jsxs)(`article`,{className:`access-record`,"aria-label":`${e.host} · ${e.id}`,children:[(0,x.jsxs)(`div`,{className:`panel-heading`,children:[(0,x.jsx)(`h3`,{children:e.host}),(0,x.jsx)(M,{tone:e.revoked_at?``:`green`,children:e.revoked_at?`已撤销`:`有效`})]}),(0,x.jsx)(`p`,{children:(0,x.jsx)(Re,{value:e.scope})}),(0,x.jsxs)(`dl`,{className:`access-facts`,children:[(0,x.jsxs)(`div`,{children:[(0,x.jsx)(`dt`,{children:`创建时间`}),(0,x.jsx)(`dd`,{children:(0,x.jsx)(N,{value:e.created_at})})]}),(0,x.jsxs)(`div`,{children:[(0,x.jsx)(`dt`,{children:`最近使用`}),(0,x.jsx)(`dd`,{children:e.last_used_at?(0,x.jsx)(N,{value:e.last_used_at}):`尚未使用`})]}),e.revoked_at&&(0,x.jsxs)(`div`,{children:[(0,x.jsx)(`dt`,{children:`撤销时间`}),(0,x.jsx)(`dd`,{children:(0,x.jsx)(N,{value:e.revoked_at})})]})]}),(0,x.jsxs)(`p`,{className:`muted`,children:[`令牌 ID:`,(0,x.jsx)(`code`,{children:e.id})]}),!e.revoked_at&&(0,x.jsxs)(`button`,{className:`danger-button`,onClick:()=>{c(``),i(e)},children:[`撤销 `,e.host,` 的令牌`]})]},e.id))}),(0,x.jsx)(`p`,{children:(0,x.jsx)(`a`,{href:`#/operations?object_type=host_token`,children:`查看令牌操作记录`})}),t&&(0,x.jsx)(Be,{close:()=>n(!1),created:e.refresh}),r&&(0,x.jsx)(ee,{title:`撤销宿主令牌`,onClose:()=>i(null),closeDisabled:a,children:(0,x.jsxs)(`div`,{className:`access-dialog`,children:[(0,x.jsxs)(`p`,{children:[`即将撤销 `,(0,x.jsx)(`strong`,{children:r.host}),` 的令牌(`,r.id,`)。后续宿主请求立即失效,无法恢复此令牌;再次接入需要创建新令牌。`]}),s&&(0,x.jsx)(A,{error:!0,children:s}),(0,x.jsxs)(`div`,{className:`button-row`,children:[(0,x.jsx)(`button`,{disabled:a,onClick:()=>i(null),children:`取消`}),(0,x.jsx)(`button`,{className:`danger-button`,disabled:a,onClick:async()=>{if(!a){o(!0),c(``);try{await g(`/tokens/${encodeURIComponent(r.id)}/revoke`,_(`POST`,{})),i(null),d(`令牌已撤销`),e.refresh()}catch(e){c(v(e))}finally{o(!1)}}},children:a?`正在撤销…`:`确认撤销`})]})]})})]})}function Be({close:e,created:t}){let n=y(`/catalog`),[r,i]=(0,l.useState)(``),[a,o]=(0,l.useState)(`entries`),[s,c]=(0,l.useState)(``),[u,d]=(0,l.useState)(``),[f,p]=(0,l.useState)(!1),[m,h]=(0,l.useState)(``),[b,S]=(0,l.useState)(null),[C,w]=(0,l.useState)(``);return(0,x.jsx)(ee,{title:b?`令牌已创建 · 仅显示这一次`:`创建宿主令牌`,onClose:e,closeDisabled:f,children:(0,x.jsx)(`div`,{className:`access-dialog`,children:b?(0,x.jsxs)(x.Fragment,{children:[(0,x.jsxs)(A,{children:[`请现在复制并交给 `,b.host,` `,`保存。关闭此窗口、离开页面或刷新后,无法再次查看令牌原文;丢失后只能撤销并重新创建。`]}),(0,x.jsxs)(`label`,{children:[`新令牌原文`,(0,x.jsx)(`textarea`,{className:`token-secret`,readOnly:!0,rows:3,spellCheck:!1,autoComplete:`off`,value:b.token,onFocus:e=>e.target.select()})]}),(0,x.jsxs)(`div`,{className:`button-row`,children:[(0,x.jsx)(`button`,{className:`primary`,onClick:async()=>{try{await navigator.clipboard.writeText(b.token),w(`已复制,请妥善保存`)}catch{w(`无法自动复制,请选择令牌原文并手动复制。`)}},children:`复制令牌`}),(0,x.jsx)(`button`,{onClick:e,children:`关闭并清除原文`})]}),C&&(0,x.jsx)(A,{children:C})]}):(0,x.jsx)(`form`,{noValidate:!0,onSubmit:async e=>{if(e.preventDefault(),f)return;let n=s.split(/\r?\n/).filter(e=>e!==``);if(!r.trim()||[...r.trim()].length>100){h(`宿主名称须为 1—100 字,不能只含空白`);return}if(a===`entries`&&(!n.length||n.length>1e3||new Set(n).size!==n.length||n.some(e=>!e.trim()||[...e].length>200||e.includes(`\0`)))){h(`请填写 1—1000 个不重复的入口 ID,每行一个,每个为 1—200 字,不能只含空白。`);return}if(a===`prefix`&&(!u.trim()||[...u].length>200||u.includes(`\0`))){h(`入口前缀须为 1—200 字,不能只含空白`);return}let i=a===`entries`?{kind:a,entries:n}:a===`prefix`?{kind:a,prefix:u}:{kind:a};p(!0),h(``);try{let e=await g(`/tokens`,_(`POST`,{host:r.trim(),scope:i}));S(e),t()}catch(e){h(`${v(e)}。如请求已提交但结果不明确,请刷新列表核对;若令牌已创建却未取得原文,请撤销后重新创建。`),t()}finally{p(!1)}},children:(0,x.jsxs)(`fieldset`,{disabled:f,children:[(0,x.jsxs)(`label`,{children:[`宿主名称`,(0,x.jsx)(`input`,{value:r,maxLength:100,onChange:e=>i(e.target.value),placeholder:`例如:本机聊天机器人`})]}),(0,x.jsxs)(`label`,{children:[`入口范围`,(0,x.jsxs)(`select`,{value:a,onChange:e=>o(e.target.value),children:[(0,x.jsx)(`option`,{value:`entries`,children:`入口列表`}),(0,x.jsx)(`option`,{value:`prefix`,children:`入口前缀`}),(0,x.jsx)(`option`,{value:`all`,children:`全部入口`})]})]}),a===`entries`&&(0,x.jsxs)(x.Fragment,{children:[!!n.data?.entries.length&&(0,x.jsxs)(`label`,{children:[`添加已有入口`,(0,x.jsxs)(`select`,{value:``,onChange:e=>{e.target.value&&!s.split(/\r?\n/).includes(e.target.value)&&c(s?`${s}\n${e.target.value}`:e.target.value)},children:[(0,x.jsx)(`option`,{value:``,children:`选择入口加入列表`}),n.data.entries.map(e=>(0,x.jsxs)(`option`,{value:e.id,children:[e.name,` · `,e.id]},e.id))]})]}),(0,x.jsxs)(`label`,{children:[`入口 ID 列表`,(0,x.jsx)(`textarea`,{rows:4,value:s,onChange:e=>c(e.target.value),placeholder:`group-a +private-a`})]}),n.error&&(0,x.jsx)(`p`,{className:`muted`,children:`已有入口暂时无法读取,仍可手动填写入口 ID。`})]}),a===`prefix`&&(0,x.jsxs)(`label`,{children:[`入口 ID 前缀`,(0,x.jsx)(`input`,{value:u,onChange:e=>d(e.target.value),maxLength:200,placeholder:`例如:astrbot:`})]}),(0,x.jsx)(`p`,{children:`列表和前缀按入口 ID 原字符匹配,区分大小写;前缀不是通配符。可以授权尚未创建的入口。范围创建后不可修改,需要调整时请新建令牌并撤销旧令牌。`}),a===`all`&&(0,x.jsx)(A,{children:`此令牌可访问全部现有及今后创建的入口。`}),m&&(0,x.jsx)(A,{error:!0,children:m}),(0,x.jsx)(`button`,{className:`primary`,disabled:f,children:f?`正在创建…`:`创建并显示令牌`})]})})})})}function Ve(e){return Number.isFinite(e)?e<1024?`${e} B`:e<1024**2?`${(e/1024).toFixed(1)} KiB`:`${(e/1024**2).toFixed(1)} MiB`:`未记录`}function He(){let e=y(`/backups?limit=30`,5e3),[t,n]=(0,l.useState)(!1),[r,i]=(0,l.useState)(!1),[a,o]=(0,l.useState)(``),[s,c]=(0,l.useState)(null),u=(0,l.useRef)(null);return(0,l.useEffect)(()=>()=>u.current?.abort(),[]),(0,l.useEffect)(()=>()=>{s&&URL.revokeObjectURL(s.url)},[s]),(0,x.jsxs)(x.Fragment,{children:[(0,x.jsxs)(`section`,{className:`panel access-panel`,"aria-label":`导出备份`,children:[(0,x.jsx)(`h2`,{children:`导出备份`}),(0,x.jsx)(`p`,{children:`服务运行时即可导出数据库与媒体的一致快照。备份包含原始消息和记忆,请妥善保存。`}),(0,x.jsxs)(`form`,{onSubmit:async n=>{if(n.preventDefault(),r)return;let a=new AbortController;u.current=a,i(!0),o(``),c(null);try{let{blob:e,headers:n}=await h(`/backups/export`,{..._(`POST`,{include_secrets:t}),signal:a.signal});if(a.signal.aborted)return;let r=n.get(`X-Iris-Backup-Includes-Secrets`)===`true`||t,i=n.get(`X-Iris-Backup-Id`),o=`iris-backup${i&&/^[a-f0-9]{32}$/.test(i)?`-${i}`:``}${r?`-with-secrets`:``}.zip`,s=URL.createObjectURL(e);c({url:s,filename:o,includesSecrets:r});let l=document.createElement(`a`);l.href=s,l.download=o,document.body.append(l),l.click(),l.remove()}catch(e){a.signal.aborted||o(v(e))}finally{a.signal.aborted||(i(!1),e.refresh())}},children:[(0,x.jsxs)(`label`,{className:`check-label`,children:[(0,x.jsx)(`input`,{type:`checkbox`,checked:t,disabled:r,onChange:e=>n(e.target.checked),"aria-describedby":`backup-key-help`}),`包含模型密钥`]}),(0,x.jsx)(`p`,{id:`backup-key-help`,className:`muted`,children:`默认不包含模型密钥。选择包含时只导出本实例已保存的密钥,不包含外部模型配置文件或环境变量中的密钥。`}),t&&(0,x.jsxs)(A,{error:!0,children:[(0,x.jsx)(`strong`,{children:`包含模型密钥的 ZIP 不加密。`}),`请只保存到可信位置,不要分享或提交到代码仓库。`]}),(0,x.jsx)(`button`,{className:`primary`,disabled:r,children:r?`正在导出,请稍候…`:t?`导出并下载(含模型密钥)`:`导出并下载备份`})]}),a&&(0,x.jsxs)(A,{error:!0,children:[a,`。如未取得备份文件,请重新导出。`]}),s&&(0,x.jsxs)(A,{children:[`已生成备份并请求浏览器下载。若下载未开始,可`,(0,x.jsx)(`a`,{href:s.url,download:s.filename,children:`保存本次备份`}),`。`,s.includesSecrets&&(0,x.jsx)(`p`,{children:(0,x.jsx)(`strong`,{children:`本次备份含未加密的模型密钥,请勿分享。`})})]}),(0,x.jsx)(`h3`,{children:`最近的导出记录`}),(0,x.jsx)(`p`,{className:`muted`,children:`最多显示最近 30 次。记录不保留可再次下载的文件,需要时请重新导出。`}),e.error&&(0,x.jsxs)(A,{error:!0,children:[e.error,(0,x.jsx)(`button`,{onClick:e.refresh,children:`重试导出记录`})]}),!e.data&&!e.error&&(0,x.jsx)(`p`,{children:`正在读取导出记录…`}),e.data?.items.length===0&&(0,x.jsx)(j,{title:`还没有导出记录`}),(0,x.jsx)(`div`,{className:`access-records`,children:e.data?.items.map(e=>(0,x.jsxs)(`article`,{className:`access-record`,children:[(0,x.jsxs)(`div`,{className:`panel-heading`,children:[(0,x.jsx)(`h4`,{children:(0,x.jsx)(N,{value:e.created_at})}),(0,x.jsx)(M,{tone:e.includes_model_secrets?`danger`:`green`,children:e.includes_model_secrets?`含模型密钥`:`不含模型密钥`})]}),(0,x.jsxs)(`p`,{children:[Ve(e.size),` · `,e.file_count??`未记录`,` 个文件 · `,e.media_included?`含媒体`:`无媒体`]}),(0,x.jsxs)(`p`,{children:[`格式版本 `,e.format_version??`未记录`,` · 迁移版本`,` `,e.migration_version??`未记录`]}),(0,x.jsxs)(`p`,{children:[(e.trigger||e.purpose)===`scheduled`?`定时`:(e.trigger||e.purpose)===`pre_import`?`导入前自动备份`:`手动导出`,` `,`·`,` `,e.actor===`admin`?`管理员`:e.actor===`local_cli`?`本机命令行`:e.actor===`scheduler`?`调度器`:e.actor]}),(0,x.jsxs)(`p`,{className:`muted`,children:[`备份 ID:`,(0,x.jsx)(`code`,{children:e.backup_id})]})]},e.backup_id))}),(0,x.jsx)(`p`,{children:(0,x.jsx)(`a`,{href:`#/operations?object_type=backup`,children:`查看导出与导入操作记录`})})]}),(0,x.jsxs)(`section`,{className:`panel access-panel`,"aria-label":`离线导入`,children:[(0,x.jsx)(`h2`,{children:`从备份导入`}),(0,x.jsx)(`p`,{children:`导入需要停止 Iris 服务,在本机命令行操作。先确认备份与目标数据目录,再按以下步骤恢复。`}),(0,x.jsxs)(`ol`,{className:`import-steps`,children:[(0,x.jsx)(`li`,{children:`停止 Iris 服务和其他配置命令。`}),(0,x.jsxs)(`li`,{children:[`将示例路径替换为自己的路径。导入到新目录:`,(0,x.jsx)(`pre`,{children:(0,x.jsx)(`code`,{children:`uv run iris --data-dir /path/to/new-data backup import /path/to/backup.zip`})})]}),(0,x.jsxs)(`li`,{children:[`覆盖已有实例时,确认目标后添加覆盖参数:`,(0,x.jsx)(`pre`,{children:(0,x.jsx)(`code`,{children:`uv run iris --data-dir /path/to/iris-data backup import /path/to/backup.zip --confirm-overwrite`})}),`覆盖前会在数据目录旁自动备份现有实例;备份失败不会覆盖。`]}),(0,x.jsx)(`li`,{children:`导入完成后按原方式启动服务,检查记忆、媒体与模型设置。`})]}),(0,x.jsx)(A,{children:`不含模型密钥的备份导入后,需要重新配置密钥,或继续使用原有外部配置;不会沿用目标目录原来的模型密钥。`}),(0,x.jsx)(`p`,{children:`覆盖前会单独保存目标实例原有的模型密钥,以便人工恢复;请私密保存该归档。它不会自动配给导入后的模型设置。`}),e.data?.import_instructions&&(0,x.jsxs)(`p`,{className:`muted`,children:[`服务提供的导入说明:`,e.data.import_instructions]})]})]})}function Ue(){let e=`http://127.0.0.1${location.port?`:${location.port}`:``}/api/v1`;return(0,x.jsxs)(x.Fragment,{children:[(0,x.jsx)(`div`,{className:`page-heading`,children:(0,x.jsxs)(`div`,{children:[(0,x.jsx)(`p`,{className:`eyebrow`,children:`接入与备份`}),(0,x.jsx)(`h1`,{children:`接入与数据`}),(0,x.jsx)(`p`,{children:`连接同一台电脑上的宿主,管理令牌与实例备份。`})]})}),(0,x.jsxs)(`section`,{className:`panel access-panel`,"aria-label":`接入说明`,children:[(0,x.jsx)(`h2`,{children:`把宿主连到 Iris`}),(0,x.jsxs)(`p`,{children:[`宿主程序与 Iris 在同一台电脑上运行。先创建令牌,在宿主配置中保存,再访问`,` `,(0,x.jsx)(`code`,{children:e}),`。端口与当前 Iris 服务一致。`]}),(0,x.jsxs)(`p`,{children:[`请求使用 `,(0,x.jsx)(`code`,{children:`Authorization: Bearer <令牌>`}),`。管理员登录会话不能代替宿主令牌;Iris 的宿主接口负责接收消息与准备回复,不生成对外回复。`]}),(0,x.jsxs)(`div`,{className:`button-row`,children:[(0,x.jsx)(`a`,{href:`/docs`,target:`_blank`,rel:`noreferrer`,children:`打开接口文档`}),(0,x.jsx)(`a`,{href:`/openapi.json`,target:`_blank`,rel:`noreferrer`,children:`查看 OpenAPI 契约`})]})]}),(0,x.jsx)(ze,{}),(0,x.jsx)(`div`,{className:`access-data-grid`,children:(0,x.jsx)(He,{})})]})}function We({value:e,openMemory:t}){return(0,x.jsxs)(`div`,{className:`trial-diagnostic`,"aria-label":`召回候选与判断分`,children:[(0,x.jsxs)(`p`,{className:`fine-print`,children:[`触发消息 #`,e.message_id,` · 准备于 `,w(e.prepared_at),(0,x.jsx)(`br`,{}),`召回记录 `,e.recall_id]}),!e.candidates.length&&(0,x.jsx)(`p`,{children:`本次没有额外召回候选。`}),e.candidates.map(e=>(0,x.jsxs)(`div`,{className:`entry-profile-evidence`,children:[(0,x.jsxs)(`button`,{className:`text-button`,onClick:()=>t(e.id),children:[`记忆 #`,e.id,` · 当时修订 `,e.revision]}),(0,x.jsxs)(`p`,{children:[e.reason===`person_highlight`?`人物要点(不参与判断)`:e.support==null?`判断分:未取得`:`判断分:${e.support} / 100`,` `,(0,x.jsx)(M,{children:{retained:`保留`,removed:`已去掉`,stale:`复核时失效`}[e.outcome]||e.outcome})]})]},e.id)),(0,x.jsx)(`p`,{className:`fine-print`,children:`以上是当次准备的诊断,链接打开记忆的当前详情;查看诊断不会再次召回或增加使用次数。`})]})}var Ge=[`image/png`,`image/jpeg`,`image/gif`,`image/webp`],Ke=e=>e>=1048576?`${(e/1048576).toFixed(1)} MiB`:`${Math.ceil(e/1024)} KiB`;function qe({media:e}){return e?.length?(0,x.jsx)(`div`,{className:`message-media`,children:e.map((e,t)=>(0,x.jsx)(Je,{item:e,number:t+1},e.id))}):null}function Je({item:e,number:t}){let[n,r]=(0,l.useState)(!1),i=e.kind===`image`&&Ge.includes(e.content_type),a=i&&e.understanding_source===`unprocessed`&&!!e.completed_at,o=i&&e.understanding_source===`unprocessed`&&!e.completed_at,s=e.kind===`audio`?`音频`:e.kind===`video`?`视频`:`文件`;return(0,x.jsxs)(`div`,{className:`media-card`,children:[(0,x.jsx)(`a`,{href:e.file_url,target:`_blank`,rel:`noopener noreferrer`,"aria-label":i?`查看图片 ${t} 原图`:`查看${s}附件 ${t}`,children:i?n?(0,x.jsx)(`span`,{children:`图片不可用或已清理`}):(0,x.jsx)(`img`,{src:e.file_url,alt:`图片 ${t}`,loading:`lazy`,onError:()=>r(!0)}):`${s}附件 ${t}`}),(0,x.jsxs)(`small`,{children:[e.content_type,` · `,Ke(e.size_bytes)]}),(0,x.jsx)(`div`,{children:(0,x.jsx)(M,{tone:a||e.understanding_source===`refused`?`danger`:``,children:e.understanding_source_label})}),a&&(0,x.jsx)(`strong`,{className:`danger-text`,children:`理解失败`}),o&&(0,x.jsxs)(x.Fragment,{children:[(0,x.jsx)(`strong`,{children:`等待理解/理解中`}),(0,x.jsx)(`small`,{children:`未启用或暂停时也会保持等待。`})]}),(0,x.jsx)(`p`,{className:`media-description`,children:e.understanding_text}),e.completed_at&&(0,x.jsxs)(`small`,{children:[`完成时间:`,w(e.completed_at)]})]})}var Ye=10485760;function Xe(e,t){return t.throwIfAborted(),new Promise((n,r)=>{let i=new FileReader,a=()=>i.abort();i.onload=()=>n(String(i.result).split(`,`,2)[1]),i.onerror=()=>r(new f(`image_read_failed`,`无法读取图片 ${e.name},请重新选择`,0)),i.onabort=()=>r(new DOMException(`Aborted`,`AbortError`)),i.onloadend=()=>t.removeEventListener(`abort`,a),t.addEventListener(`abort`,a,{once:!0}),i.readAsDataURL(e)})}function Ze(){let[e,t]=(0,l.useState)([]),n=(0,l.useRef)([]),r=(0,l.useRef)(new Map),i=e=>{n.current=e,t(e)};(0,l.useEffect)(()=>()=>{for(let e of n.current)URL.revokeObjectURL(e.url);r.current.clear()},[]);function a(e){if(n.current.length+e.length>100)throw new f(`too_many_images`,`每条消息最多发送 100 张图片`,0);for(let t of e){if(!Ge.includes(t.type))throw new f(`unsupported_media_type`,`${t.name}:仅支持 PNG、JPEG、GIF、WebP 图片`,0);if(t.size>Ye)throw new f(`media_too_large`,`${t.name}:图片超过单文件 10 MiB 上限`,413);if(!t.size)throw new f(`empty_image`,`${t.name}:图片文件为空`,0)}i([...n.current,...e.map(e=>({file:e,key:crypto.randomUUID(),url:URL.createObjectURL(e)}))])}function o(e){let t=n.current.find(t=>t.key===e);t&&URL.revokeObjectURL(t.url),r.current.delete(e),i(n.current.filter(t=>t.key!==e))}function s(){for(let e of n.current)URL.revokeObjectURL(e.url);r.current.clear(),i([])}async function c(e,t){let i=[],a=n.current;for(let[n,o]of a.entries()){e.throwIfAborted();let s=r.current.get(o.key);if(!s){t(`正在上传图片 ${n+1}/${a.length}…`);let i=await Xe(o.file,e);e.throwIfAborted();let c=await g(`/media`,{..._(`POST`,{content_type:o.file.type,data_base64:i}),signal:e});e.throwIfAborted(),s=c.id,r.current.set(o.key,s)}i.push(s)}return i}return{images:e,add:a,remove:o,clear:s,upload:c}}function Qe({images:e,busy:t,add:n,remove:r}){return(0,x.jsxs)(`div`,{className:`trial-images`,children:[(0,x.jsxs)(`label`,{children:[`选择图片`,(0,x.jsx)(`input`,{type:`file`,accept:Ge.join(`,`),multiple:!0,disabled:t,onChange:e=>{n(Array.from(e.target.files||[])),e.target.value=``}})]}),(0,x.jsx)(`p`,{className:`fine-print`,children:`也可在消息框粘贴图片。PNG/JPEG/GIF/WebP,每张最多 10 MiB,可只发送图片。`}),!!e.length&&(0,x.jsx)(`ul`,{className:`draft-images`,"aria-label":`待发送图片`,children:e.map(e=>(0,x.jsxs)(`li`,{children:[(0,x.jsx)(`img`,{src:e.url,alt:`待发送图片:${e.file.name}`}),(0,x.jsxs)(`span`,{children:[e.file.name,` · `,Ke(e.file.size)]}),(0,x.jsx)(`button`,{type:`button`,disabled:t,onClick:()=>r(e.key),"aria-label":`移除图片 ${e.file.name}`,children:`移除`})]},e.key))})]})}function $e(){let e=y(`/persona`,2e3),t=e.data;return(0,x.jsxs)(`section`,{className:`persona-summary`,"aria-label":`试用 persona`,children:[e.error&&(0,x.jsxs)(A,{error:!0,children:[e.error,`。persona 可能尚未更新。`,(0,x.jsx)(`button`,{onClick:e.refresh,children:`重试 persona`})]}),t?.needs_update&&(0,x.jsxs)(`p`,{children:[(0,x.jsx)(M,{tone:`warning`,children:`待更新`}),` `,t.stale_basis_count,` `,`条依据已失效,当前版本仍在使用。`]}),t?.pending&&(0,x.jsxs)(A,{children:[(0,x.jsx)(M,{tone:`warning`,children:`待确认`}),` `,(0,x.jsx)(`strong`,{children:`1 个 persona 候选等待确认`}),(0,x.jsxs)(`p`,{children:[`候选 v`,t.pending.id,` 尚未生效,当前版本仍在使用。`]}),(0,x.jsx)(`a`,{href:`#/persona?tab=current&focus=pending`,children:`查看候选与差异`})]}),(0,x.jsxs)(`details`,{open:!0,children:[(0,x.jsxs)(`summary`,{children:[`当前 persona`,t?.current?` · v${t.current.id}`:``]}),(0,x.jsx)(`p`,{className:`persona-content`,children:t?t.current?.content||`暂无 persona 版本`:`正在读取 persona…`})]}),(0,x.jsx)(`a`,{href:`#/persona`,children:`查看 persona 与自我`})]})}function et(e,t){let n=t.find(t=>t.id===e);return n?`${n.name}(${e})`:e}function tt(e,t){return e.visibility===`shared`?`全部入口共享(默认)`:e.visibility===`entry_only`?`仅本入口`:e.visibility===`entries`?`指定入口:${e.visible_in.map(e=>et(e,t)).join(`、`)}`:`未返回,请重新加载`}function nt(e,t){return e?e.shared?`全部入口`:e.visible_in.length?e.visible_in.map(e=>et(e,t)).join(`、`):`仅管理员可见`:`未返回,请重新加载详情`}function rt({entry:e,entries:t,value:n,busy:r,setBusy:i,onSaved:a}){let[o,s]=(0,l.useState)(n.visibility),[c,u]=(0,l.useState)(n.visible_in.filter(t=>t!==e.id)),[d,f]=(0,l.useState)(``);async function p(t){if(t.preventDefault(),!r){i(!0),f(``);try{await g(`/entries/${encodeURIComponent(e.id)}/visibility`,_(`PATCH`,{visibility:o,visible_in:o===`entries`?c:[]})),a()}catch(e){f(v(e))}finally{i(!1)}}}return(0,x.jsxs)(`form`,{className:`entry-settings-form entry-visibility-form`,onSubmit:p,children:[(0,x.jsx)(`h3`,{children:`记忆可见范围`}),(0,x.jsx)(`p`,{className:`muted`,id:`entry-visibility-help`,children:`这是查询时生效的设置。修改后立即影响该入口来源的全部已有记忆,无需重新学习,可随时改回。 多个来源或派生依据的限制共同生效,最终范围可在记忆详情查看。`}),d&&(0,x.jsx)(A,{error:!0,children:d}),(0,x.jsxs)(`fieldset`,{className:`lifecycle-fieldset`,disabled:r,children:[(0,x.jsxs)(`label`,{children:[`记忆可见范围`,(0,x.jsxs)(`select`,{value:o,"aria-describedby":`entry-visibility-help`,onChange:e=>s(e.target.value),children:[(0,x.jsx)(`option`,{value:`shared`,children:`全部入口共享(默认)`}),(0,x.jsx)(`option`,{value:`entry_only`,children:`仅本入口`}),(0,x.jsx)(`option`,{value:`entries`,children:`指定入口`})]})]}),o===`entries`&&(0,x.jsxs)(`fieldset`,{className:`lifecycle-fieldset`,children:[(0,x.jsx)(`legend`,{children:`选择可见入口(可多选)`}),(0,x.jsxs)(`label`,{className:`check-label`,children:[(0,x.jsx)(`input`,{type:`checkbox`,checked:!0,disabled:!0}),et(e.id,[e]),` · 本入口始终包含`]}),t.filter(t=>t.id!==e.id).map(e=>(0,x.jsxs)(`label`,{className:`check-label`,children:[(0,x.jsx)(`input`,{type:`checkbox`,checked:c.includes(e.id),onChange:t=>{u(t.target.checked?[...c,e.id]:c.filter(t=>t!==e.id))}}),et(e.id,t)]},e.id)),t.every(t=>t.id===e.id)&&(0,x.jsx)(`p`,{className:`muted`,children:`暂无其他入口可选。`})]}),(0,x.jsx)(`p`,{className:`fine-print`,children:`可见范围与下方的节奏、过滤分别保存。`}),(0,x.jsx)(`button`,{className:`primary`,disabled:r,children:r?`正在保存…`:`保存可见范围`})]})]})}var it={realtime:{name:`实时`,value:{count:4,idle_seconds:5,max_wait_seconds:60}},standard:{name:`标准`,value:{count:12,idle_seconds:600,max_wait_seconds:3600}},economy:{name:`省流`,value:{count:30,idle_seconds:1800,max_wait_seconds:14400}}};function at(e){if(typeof e==`string`&&it[e])return it[e].name;try{let t=typeof e==`string`?JSON.parse(e):e;return`自定义 · ${t.count} 条 / 空闲 ${t.idle_seconds} 秒 / 最长 ${t.max_wait_seconds} 秒`}catch{return`未知节奏`}}function ot({value:e}){return e?(0,x.jsxs)(`div`,{className:`entry-filter-summary`,children:[(0,x.jsxs)(`p`,{children:[`最短字数:`,e.min_chars?`${e.min_chars} 字`:`不限制`]}),(0,x.jsxs)(`p`,{children:[`提及过滤:`,e.mention_only?`只学习提到角色的消息及前后各 ${e.context_messages} 条`:`关闭`]}),(0,x.jsxs)(`p`,{children:[`每小时批次上限:`,e.max_batches_per_hour||`不限制`]})]}):null}function st({value:e}){return e?(0,x.jsxs)(`div`,{className:`entry-queue-wait`,children:[e.reason&&(0,x.jsx)(`p`,{className:`queue-reason`,children:e.reason===`hourly_batch_limit`?`达到每小时批次上限,消息排队等待`:e.reason===`filter_context`?`等待提及窗口的后续消息或空闲定案`:e.reason}),e.reason===`hourly_batch_limit`&&e.retry_at&&(0,x.jsxs)(`p`,{children:[`最早释放额度:`,w(e.retry_at),`(不是完成时间)`]}),e.batches_last_hour!==null&&(0,x.jsxs)(`p`,{className:`muted`,children:[`滚动 60 分钟已组成 `,e.batches_last_hour,` / `,e.limit,` 个批次`]}),(0,x.jsxs)(`p`,{className:`muted`,children:[`待过滤定案 `,e.filter_waiting_count,` 条 · 已过滤`,` `,e.filtered_count,` 条`]})]}):null}function ct({entry:e,entries:t,onClose:n,onSaved:r}){let i=y(`/entries/${encodeURIComponent(e.id)}/settings`),[a,o]=(0,l.useState)(!1);return(0,x.jsxs)(ee,{title:`入口设置 · ${e.name}`,onClose:n,closeDisabled:a,children:[i.error&&(0,x.jsxs)(A,{error:!0,children:[i.error,(0,x.jsx)(`button`,{onClick:i.refresh,children:`重新加载`})]}),i.data?(0,x.jsxs)(x.Fragment,{children:[i.data.visibility?(0,x.jsx)(rt,{entry:e,entries:t,value:i.data,busy:a,setBusy:o,onSaved:r}):(0,x.jsxs)(A,{error:!0,children:[`未读到可见范围,请重新加载入口设置。`,(0,x.jsx)(`button`,{onClick:i.refresh,children:`重新加载`})]}),(0,x.jsx)(lt,{entry:e,value:i.data,busy:a,setBusy:o,onSaved:r})]}):(0,x.jsx)(`p`,{role:`status`,children:`正在读取入口设置…`})]})}function lt({entry:e,value:t,busy:n,setBusy:r,onSaved:i}){let[a,o]=(0,l.useState)(typeof t.pace==`string`?t.pace:`custom`),s=typeof t.pace==`string`?it[t.pace].value:t.pace,[c,u]=(0,l.useState)({count:String(s.count),idle_seconds:String(s.idle_seconds),max_wait_seconds:String(s.max_wait_seconds)}),[d,f]=(0,l.useState)({min_chars:String(t.filters.min_chars),mention_only:t.filters.mention_only,context_messages:String(t.filters.context_messages),max_batches_per_hour:String(t.filters.max_batches_per_hour)}),[p,m]=(0,l.useState)(``),h=[[`min_chars`,`最短字数`,32768],[`context_messages`,`前后各 K 条消息`,100],[`max_batches_per_hour`,`每小时最多批次`,1e3]],y=[[`count`,`触发条数`,1e3],[`idle_seconds`,`空闲秒数`,86400],[`max_wait_seconds`,`最长等待秒数`,604800]];async function b(t){if(t.preventDefault(),!n){for(let[e,t,n]of h){let r=Number(d[e]);if(!d[e].trim()||!Number.isInteger(r)||r<0||r>n){m(`${t}须为 0—${n} 的整数`);return}}if(a===`custom`)for(let[e,t,n]of y){let r=Number(c[e]);if(!c[e].trim()||!Number.isInteger(r)||r<1||r>n){m(`${t}须为 1—${n} 的整数`);return}}r(!0),m(``);try{await g(`/entries/${encodeURIComponent(e.id)}/settings`,_(`PATCH`,{pace:a===`custom`?{count:Number(c.count),idle_seconds:Number(c.idle_seconds),max_wait_seconds:Number(c.max_wait_seconds)}:a,filters:{min_chars:Number(d.min_chars),mention_only:d.mention_only,context_messages:Number(d.context_messages),max_batches_per_hour:Number(d.max_batches_per_hour)}})),i()}catch(e){m(v(e))}finally{r(!1)}}}return(0,x.jsxs)(`form`,{className:`entry-settings-form`,onSubmit:b,noValidate:!0,children:[p&&(0,x.jsx)(A,{error:!0,children:p}),(0,x.jsxs)(`fieldset`,{className:`lifecycle-fieldset`,disabled:n,children:[(0,x.jsxs)(`label`,{children:[`学习节奏`,(0,x.jsxs)(`select`,{value:a,onChange:e=>o(e.target.value),children:[Object.entries(it).map(([e,t])=>(0,x.jsx)(`option`,{value:e,children:t.name},e)),(0,x.jsx)(`option`,{value:`custom`,children:`自定义`})]})]}),a===`custom`?(0,x.jsx)(`div`,{className:`form-grid`,children:y.map(([e,t,n])=>(0,x.jsxs)(`label`,{children:[t,(0,x.jsx)(`input`,{type:`number`,min:1,max:n,step:1,required:!0,value:c[e],onChange:t=>u({...c,[e]:t.target.value})})]},e))}):(0,x.jsxs)(`p`,{className:`muted`,children:[it[a].value.count,` 条 / 空闲`,` `,it[a].value.idle_seconds,` 秒 / 最长等待`,` `,it[a].value.max_wait_seconds,` 秒`]}),(0,x.jsx)(`h3`,{children:`高流量入口过滤`}),(0,x.jsxs)(`label`,{children:[`最短字数`,(0,x.jsx)(`input`,{"aria-label":`最短字数`,"aria-describedby":`min-chars-help`,type:`number`,min:0,max:32768,step:1,required:!0,value:d.min_chars,onChange:e=>f({...d,min_chars:e.target.value})})]}),(0,x.jsx)(`small`,{id:`min-chars-help`,children:`0 表示关闭;按正文去掉首尾空白后的字符数计算。`}),(0,x.jsxs)(`label`,{className:`check-label`,children:[(0,x.jsx)(`input`,{type:`checkbox`,checked:d.mention_only,onChange:e=>f({...d,mention_only:e.target.checked})}),`只学习提到角色的消息`]}),(0,x.jsx)(`p`,{className:`muted`,children:`正文中出现角色名或已确认的角色别名才算提及;引用内容、发送者名字和“记住”本身不算。`}),(0,x.jsxs)(`label`,{children:[`前后各 K 条消息`,(0,x.jsx)(`input`,{"aria-label":`前后各 K 条消息`,"aria-describedby":`context-help`,type:`number`,min:0,max:100,step:1,required:!0,disabled:!d.mention_only,value:d.context_messages,onChange:e=>f({...d,context_messages:e.target.value})})]}),(0,x.jsx)(`small`,{id:`context-help`,children:`0 表示仅学习提及消息;按本入口原始消息顺序计数,等待后续消息或空闲后定案。`}),(0,x.jsxs)(`label`,{children:[`每小时最多批次`,(0,x.jsx)(`input`,{"aria-label":`每小时最多批次`,"aria-describedby":`hourly-help`,type:`number`,min:0,max:1e3,step:1,required:!0,value:d.max_batches_per_hour,onChange:e=>f({...d,max_batches_per_hour:e.target.value})})]}),(0,x.jsx)(`small`,{id:`hourly-help`,children:`0 表示关闭;按滚动 60 分钟内新组成的批次计数。立即学习也受此上限及提及窗口限制。`}),(0,x.jsx)(A,{children:`设置立即作用于尚未冻结的待处理消息,已组成批次保持原样。被过滤的消息仍保存并可用于近期对话,但不会进入学习、不算积压或记忆缺口;放宽或关闭过滤不会重新学习这些消息。`})]}),(0,x.jsx)(`button`,{className:`primary`,disabled:n,children:n?`正在保存…`:`保存节奏与过滤`})]})}var ut={no_candidates:`没有需要判断的相关候选`,host_disabled:`本次请求已关闭判断`,configuration_disabled:`设置中已关闭判断`,unconfigured:`未配置判断模型`,configuration_error:`模型配置错误`,configuration:`模型配置或响应无效`,configuration_changed:`调用期间模型配置发生变化`,invalid_key:`密钥无效`,authentication:`密钥无效`,account_problem:`模型账户异常`,account:`模型账户异常`,usage_limit:`达到每日 token 上限`,rate_limited:`服务商限流,正在退避`,temporarily_unavailable:`模型用途已暂停`,paused:`模型用途已暂停`,queue_full:`判断排队已满`,queue_timeout:`排队超时`,timeout:`超过判断总预算`,retryable:`模型连接暂时失败`,invalid_output:`判断输出格式无效`,content_rejection:`模型拒绝判断内容`};function dt({value:e}){return e?(0,x.jsxs)(`div`,{className:`judgment-summary`,"aria-label":`本次召回判断`,children:[(0,x.jsxs)(`p`,{children:[(0,x.jsx)(`strong`,{children:`召回判断:`}),(0,x.jsx)(M,{tone:e.status===`degraded`?`danger`:e.status===`applied`?`green`:``,children:{applied:`正常`,disabled:`已关闭`,degraded:`降级`}[e.status]||e.status})]}),e.reason&&(0,x.jsx)(`p`,{children:ut[e.reason]||`判断原因:${e.reason}`}),(0,x.jsxs)(`p`,{children:[`被判断去掉 `,e.removed_memory_ids.length,` 条记忆`]}),!!e.stale_memory_ids?.length&&(0,x.jsxs)(`p`,{children:[`另有 `,e.stale_memory_ids.length,` 条记忆在复核时失效`]}),(0,x.jsxs)(`p`,{className:`muted`,children:[`耗时 `,C(e.duration_ms),` · 总预算 `,e.budget_seconds,` `,`秒(含排队)`]}),e.status===`degraded`&&(0,x.jsx)(`p`,{children:`本次保留仍有效的原召回。`})]}):(0,x.jsx)(`p`,{className:`fine-print`,children:`本次未返回召回判断状态。`})}function ft({value:e}){return e?(0,x.jsxs)(`div`,{className:`subject-annotations`,children:[e.possible_same_as.map(e=>(0,x.jsxs)(`p`,{children:[`可能是同一人:`,e.subjects.map(e=>e.name).join(`、`),` · 相信程度`,` `,e.belief,` / 100。`,(0,x.jsx)(`a`,{href:`#/people?id=${encodeURIComponent(e.subjects[0].id)}`,children:`核对人物联系`})]},e.link_id)),e.roleplay.map(e=>(0,x.jsxs)(`p`,{children:[`扮演关系(虚构):`,e.actor.name,` 扮演 `,e.character.name,` ·`,` `,e.worlds.map(e=>e??`场景未知`).join(`、`),` · 相信程度`,` `,e.belief,` / 100`]},e.link_id))]}):null}var pt={open:`未结束`,completed:`已完成`,abandoned:`已放弃`},mt={normal:`普通目标`,question:`询问`},ht={internal:`内部`,host:`宿主`,admin:`管理员`},gt={soon:`临近提醒`,due:`到期提醒`,overdue:`过期提醒`,immediate:`即时提醒`},_t={pending:`待取走`,taken:`已取走`,cancelled:`已取消`},vt={created:`新建`,possible_duplicate:`可能重复,已分别保留`,merged:`已合并`,pending:`去重待复核,已保留`},yt=(e,t)=>{let n=t?.people.find(t=>t.id===e);return n?`${n.name}(${e})`:e},bt=(e,t)=>e?t?.entries.find(t=>t.id===e)?.name||e:`未指定`;function xt({goal:e}){return(0,x.jsxs)(`div`,{className:`detail-badges goal-badges`,children:[(0,x.jsx)(M,{children:mt[e.kind]}),(0,x.jsx)(M,{children:pt[e.state]}),(0,x.jsx)(M,{children:ht[e.origin]}),e.overdue?(0,x.jsx)(M,{tone:`danger`,children:`已过期`}):e.due_soon?(0,x.jsx)(M,{tone:`purple`,children:`临近截止`}):null,e.possible_duplicate&&(0,x.jsx)(M,{tone:`warning`,children:`可能重复`}),e.basis_needs_review&&(0,x.jsx)(M,{tone:`warning`,children:`依据可能不成立`}),e.auto_closed&&(0,x.jsx)(M,{tone:`purple`,children:e.state===`abandoned`?`维护自动放弃`:`维护自动完成`}),e.merged_into&&(0,x.jsx)(M,{children:`已合并`})]})}function St({goal:e}){return e.kind===`question`?(0,x.jsx)(x.Fragment,{children:`询问不设截止时间`}):e.deadline?e.deadline_unresolved?(0,x.jsxs)(`span`,{className:`danger-text`,children:[e.deadline,`(时间待明确,不安排提醒)`]}):(0,x.jsx)(N,{value:e.deadline}):(0,x.jsx)(x.Fragment,{children:`未设截止时间`})}function Ct({goal:e,catalog:t}){return(0,x.jsxs)(`dl`,{className:`goal-facts`,children:[(0,x.jsxs)(`div`,{children:[(0,x.jsx)(`dt`,{children:`截止时间`}),(0,x.jsx)(`dd`,{children:(0,x.jsx)(St,{goal:e})})]}),(0,x.jsxs)(`div`,{children:[(0,x.jsx)(`dt`,{children:`提醒提前量`}),(0,x.jsx)(`dd`,{children:e.kind===`question`?`询问不安排提醒`:`${e.effective_reminder_minutes} 分钟${e.reminder_minutes===null?`(使用默认值)`:``}`})]}),(0,x.jsxs)(`div`,{children:[(0,x.jsx)(`dt`,{children:`涉及的人`}),(0,x.jsx)(`dd`,{children:e.people.map(e=>yt(e,t)).join(`、`)||`未指定`})]}),(0,x.jsxs)(`div`,{children:[(0,x.jsx)(`dt`,{children:`产生入口`}),(0,x.jsx)(`dd`,{children:bt(e.entry_id,t)})]})]})}function wt({receipt:e,openGoal:t}){return(0,x.jsxs)(`div`,{className:`notice`,role:`status`,children:[(0,x.jsxs)(`p`,{children:[`已创建目标 #`,e.submitted_id,` · `,vt[e.dedup.status]]}),(0,x.jsxs)(`button`,{className:`text-button`,onClick:()=>t(e.goal.id),children:[`查看目标 #`,e.goal.id]}),e.dedup.target_id&&e.dedup.target_id!==e.goal.id&&(0,x.jsxs)(`button`,{className:`text-button`,onClick:()=>t(e.dedup.target_id),children:[`查看可能重复的目标 #`,e.dedup.target_id]})]})}var Tt={auto_completed:`依据明确,自动完成`,auto_abandoned:`依据明确,自动放弃`,decision_open:`判断仍未结束`,invalid_output:`模型输出无效,保持未结束`,content_rejection:`模型拒绝处理,保持未结束`,evidence_empty:`未给出依据,保持未结束`,evidence_outside_material:`依据不在材料内,保持未结束`,evidence_only_sources:`依据只有目标自己的出处消息,保持未结束`,evidence_missing:`依据消息已不存在,保持未结束`,evidence_not_after_creation:`依据不晚于目标创建或重开,保持未结束`,evidence_sender_not_involved:`依据不是涉及的人或角色所说,保持未结束`,evidence_kind_not_allowed:`依据只有场景事件,保持未结束`,auto_merged:`判为重复,已合并到较早的目标`,auto_different:`判为不重复,已撤销可能重复标记`,admin_not_merged:`判为重复,但含管理员创建的目标,不合并并撤销标记`,host_scope_not_merged:`宿主目标会被并入它令牌范围外的目标,不合并,保留可能重复标记`,uncertain:`仍不确定,保留可能重复标记`,same_ambiguous:`判为重复但还有其他歧义,保留标记`,partner_not_in_material:`对方不在去重材料中,保留标记`,partner_not_judged:`没有得到对方的判断,保留标记`,merge_blocked:`不满足合并条件,保留标记`,no_candidates:`没有新的候选消息,未调用模型`,already_judged:`目标与材料未变,未再次调用模型`,budget_exhausted:`预算用尽,留到下次`,merged_this_run:`本轮刚被自动合并,下次维护再复核`,revision_conflict:`目标在判断期间已变化,下次再处理`,dedup_judge_disabled:`目标去重判断已关闭`,review_pending:`创建时的去重复核尚未结束`,pair_changed:`可能重复关系已变化`,disabled:`开关已关闭`,timeout:`超时,下次再试`,rate_limited:`限流,下次再试`,paused:`模型用途已暂停`,usage_limit:`达到每日 token 上限,下次再试`,merge_undone:`自动合并已撤销`,unconfigured:`模型未配置`},Et=e=>e?Tt[e]||e:``;function Dt({goal:e,locked:t,openMemory:n,clear:r}){return e.basis_annotations?.length?(0,x.jsxs)(`section`,{className:`panel goals-panel`,"aria-label":`目标依据复核`,children:[(0,x.jsx)(`h2`,{children:`依据可能不成立`}),(0,x.jsx)(`p`,{className:`goal-help`,children:`依据记忆已变化,请核对目标是否仍然成立;标注不会自动结束目标。`}),e.basis_annotations.map(i=>(0,x.jsxs)(`article`,{className:`goal-operation`,children:[(0,x.jsx)(`p`,{className:`report-text`,children:i.text}),(0,x.jsxs)(`p`,{children:[`依据修订 `,i.basis_revision,` · 观察到修订`,` `,i.observed_revision??`未知`]}),(0,x.jsxs)(`p`,{className:`muted`,children:[`复核时间:`,(0,x.jsx)(N,{value:i.created_at})]}),(0,x.jsxs)(`div`,{className:`actions`,children:[(0,x.jsxs)(`button`,{className:`text-button`,onClick:()=>n(i.memory_id),children:[`查看依据记忆 #`,i.memory_id]}),i.observed_merged_into!=null&&(0,x.jsxs)(`button`,{className:`text-button`,onClick:()=>n(i.observed_merged_into),children:[`已合并到记忆 #`,i.observed_merged_into]}),!e.merged_into&&(0,x.jsxs)(`button`,{disabled:t,onClick:()=>r(i),children:[`清除依据标注 #`,i.id]})]})]},i.id))]}):null}function Ot({goal:e,openGoal:t}){let n=e.dedup_review;return(0,x.jsxs)(`section`,{className:`panel goals-panel`,"aria-label":`目标去重判断`,children:[(0,x.jsx)(`h2`,{children:`目标去重判断`}),n?(0,x.jsxs)(x.Fragment,{children:[(0,x.jsx)(M,{tone:n.state===`done`?`green`:`warning`,children:{pending:`待复核`,running:`复核中`,done:`已判断`,cancelled:`已取消`}[n.state]}),(0,x.jsxs)(`p`,{children:[`方法 `,n.method,` · 输入修订 `,n.input_revision,` · 尝试`,` `,n.attempts,` 次`]}),n.result.status&&(0,x.jsxs)(`p`,{children:[`判断结果:`,vt[n.result.status]||n.result.status]}),n.result.reason&&(0,x.jsx)(`p`,{children:pe(n.result.reason)}),n.result.target_id!=null&&(0,x.jsxs)(`button`,{className:`text-button`,onClick:()=>t(n.result.target_id),children:[`查看判断关联目标 #`,n.result.target_id]}),n.next_attempt_at&&(0,x.jsxs)(`p`,{children:[`下次复核:`,(0,x.jsx)(N,{value:n.next_attempt_at})]}),(0,x.jsxs)(`p`,{className:`muted`,children:[`最后更新:`,(0,x.jsx)(N,{value:n.updated_at})]}),[`pending`,`running`].includes(n.state)&&(0,x.jsx)(`p`,{className:`goal-help`,children:`已保留目标,等待模型复核;待复核不表示已经判定重复。`})]}):(0,x.jsx)(`p`,{className:`goal-help`,children:`没有异步复核任务记录。可能重复关系以当前目标标记为准。`}),(0,x.jsx)(`p`,{className:`goal-help`,children:`管理员手动创建的目标不会自动合并,判断为重复也只标记“可能重复”。`})]})}var kt={completed:`完成`,abandoned:`放弃`,open:`未结束`,invalid:`输出无效`},At={merged:`已合并`,separated:`已撤销可能重复`,kept:`保留可能重复`};function jt({judgment:e,busy:t,error:n,confirm:r,cancel:i}){let a=e.target_id===e.goal_a?e.goal_b:e.goal_a;return(0,x.jsxs)(ee,{title:`撤销自动合并`,closeDisabled:t,onClose:i,children:[n&&(0,x.jsx)(A,{error:!0,children:n}),(0,x.jsxs)(`div`,{className:`goal-confirm`,children:[(0,x.jsxs)(`p`,{children:[`恢复目标 #`,a,`,保留目标 #`,e.target_id,`的截止时间、提前量、来源和提醒退回合并前的状态。`]}),(0,x.jsx)(`p`,{children:`保留目标在合并后被修改过时不能撤销。撤销后这两条目标不再被自动合并。`}),(0,x.jsxs)(`div`,{className:`actions`,children:[(0,x.jsx)(`button`,{className:`primary`,disabled:t,onClick:r,children:`确认撤销合并`}),(0,x.jsx)(`button`,{className:`secondary`,disabled:t,onClick:i,children:`取消`})]})]})]})}function Mt({messages:e}){return e.length?(0,x.jsx)(`ul`,{className:`goal-evidence`,children:e.map(e=>(0,x.jsx)(`li`,{children:e.missing?(0,x.jsxs)(`span`,{className:`muted`,children:[`消息 #`,e.id,`:`,e.notice||`消息已清理或不可用`]}):(0,x.jsxs)(x.Fragment,{children:[(0,x.jsxs)(`span`,{className:`muted`,children:[`消息 #`,e.id,` · `,e.sender_name||e.sender_subject_id,` ·`,` `,e.entry_name||e.entry_id,` ·`,` `,(0,x.jsx)(N,{value:e.occurred_at||null})]}),(0,x.jsx)(`p`,{className:`goal-content`,children:e.content})]})},e.id))}):(0,x.jsx)(`p`,{className:`goal-help`,children:`没有依据消息。`})}function Nt({goal:e,locked:t,reopen:n,undoMerge:r,openGoal:i}){let a=e.auto_review;return!a||!a.closure&&!a.reviews.length&&!a.duplicate_judgments.length?null:(0,x.jsxs)(`section`,{className:`panel goals-panel`,"aria-label":`每日维护自动复核`,children:[(0,x.jsx)(`h2`,{children:`每日维护自动复核`}),a.closure&&(0,x.jsxs)(`article`,{className:`goal-operation`,children:[(0,x.jsxs)(`p`,{children:[(0,x.jsxs)(M,{tone:`purple`,children:[`自动`,kt[a.closure.decision]]}),` `,Et(a.closure.reason),` ·`,` `,(0,x.jsx)(N,{value:a.closure.created_at})]}),(0,x.jsx)(`h3`,{children:`依据消息`}),(0,x.jsx)(Mt,{messages:a.closure.evidence_messages||[]}),a.reopenable&&(0,x.jsx)(`div`,{className:`actions`,children:(0,x.jsx)(`button`,{disabled:t,onClick:n,children:`重开目标`})})]}),a.reviews.length>0&&(0,x.jsxs)(`details`,{children:[(0,x.jsxs)(`summary`,{children:[`复核记录 · `,a.reviews.length,` 条`]}),a.reviews.map(e=>(0,x.jsxs)(`p`,{children:[(0,x.jsx)(N,{value:e.created_at}),` · 修订 `,e.goal_revision,` · 判断`,kt[e.decision],` · `,Et(e.reason),e.evidence.length>0&&` · 依据消息 ${e.evidence.map(e=>`#`+e).join(`、`)}`]},e.id))]}),a.duplicate_judgments.length>0&&(0,x.jsxs)(`details`,{children:[(0,x.jsxs)(`summary`,{children:[`可能重复补判 · `,a.duplicate_judgments.length,` 条`]}),a.duplicate_judgments.map(n=>{let a=n.goal_a===e.id?n.goal_b:n.goal_a;return(0,x.jsxs)(`div`,{children:[(0,x.jsxs)(`p`,{children:[(0,x.jsx)(N,{value:n.created_at}),` · 与目标 #`,a,` ·`,` `,At[n.outcome],` · `,Et(n.reason)]}),(0,x.jsxs)(`div`,{className:`actions`,children:[(0,x.jsxs)(`button`,{className:`text-button`,onClick:()=>i(a),children:[`查看目标 #`,a]}),n.undoable&&(0,x.jsxs)(`button`,{disabled:t,onClick:()=>r(n),children:[`撤销合并 #`,n.id]}),n.undone_at&&(0,x.jsx)(`span`,{className:`muted`,children:`已撤销`})]})]},n.id)})]}),(0,x.jsx)(`p`,{className:`goal-help`,children:`只有学习产生的普通目标会按依据自动完成或放弃;宿主和管理员创建的目标不会被自动关闭。重开后,只有更晚的新消息才可能让它再次被关闭。`})]})}function Pt({goal:e,busy:t,error:n,confirm:r,cancel:i}){return(0,x.jsxs)(ee,{title:`重开目标`,closeDisabled:t,onClose:i,children:[n&&(0,x.jsx)(A,{error:!0,children:n}),(0,x.jsxs)(`div`,{className:`goal-confirm`,children:[(0,x.jsx)(`p`,{children:e.content}),(0,x.jsx)(`p`,{children:`重开后目标恢复未结束并重新安排提醒。之后每日维护只会参考比这次重开更晚的消息,不会凭原来的依据再次关闭它。`}),(0,x.jsxs)(`div`,{className:`actions`,children:[(0,x.jsx)(`button`,{className:`primary`,disabled:t,onClick:r,children:`确认重开`}),(0,x.jsx)(`button`,{className:`secondary`,disabled:t,onClick:i,children:`取消`})]})]})]})}function Ft({openGoal:e,onChange:t}){let[n,r]=(0,l.useState)(!1),[i,a]=(0,l.useState)(0),o=y(n?`/goal-auto-actions?limit=10&offset=${i}`:null),[s,c]=(0,l.useState)(null),[u,d]=(0,l.useState)(null),[p,m]=(0,l.useState)(!1),[h,b]=(0,l.useState)(``),[S,C]=(0,l.useState)(``);async function w(){if(s&&!p){m(!0),b(``);try{await g(`/goals/${s.id}/reopen`,_(`POST`,{expected_revision:s.revision})),C(`目标 #${s.id} 已重开`),c(null),o.refresh(),t()}catch(e){b(e instanceof f&&e.status===409?`目标已变化,请刷新后重新核对。`:v(e))}finally{m(!1)}}}async function E(){if(u&&!p){m(!0),b(``);try{await g(`/goal-duplicate-judgments/${u.id}/undo`,_(`POST`,{expected_revision:u.target_revision})),C(`已撤销目标 #${u.goal_a} 与 #${u.goal_b} 的自动合并`),d(null),o.refresh(),t()}catch(e){b(e instanceof f&&e.status===409?`保留目标在合并后已变化,不能撤销;请到目标页核对。`:v(e))}finally{m(!1)}}}return(0,x.jsxs)(`section`,{className:`panel goals-panel`,"aria-label":`每日维护自动处理`,children:[(0,x.jsx)(`button`,{className:`text-button`,"aria-expanded":n,onClick:()=>r(e=>!e),children:`每日维护自动处理的目标`}),n&&(0,x.jsxs)(x.Fragment,{children:[(0,x.jsx)(`p`,{className:`goal-help`,children:`列出每日维护按依据自动完成、放弃的目标,以及可能重复补判后合并或撤销标记的目标。`}),S&&(0,x.jsx)(A,{children:S}),o.error&&(0,x.jsx)(A,{error:!0,children:o.error}),!o.data&&!o.error&&(0,x.jsx)(`p`,{role:`status`,children:`正在读取…`}),o.data?.items.length===0&&(0,x.jsx)(`p`,{className:`goal-help`,children:`暂无自动处理记录。`}),o.data?.items.map(t=>t.type===`review`?(0,x.jsxs)(`article`,{className:`goal-operation`,children:[(0,x.jsxs)(`p`,{children:[(0,x.jsxs)(M,{tone:`purple`,children:[`自动`,kt[t.decision]]}),` `,`目标 #`,t.goal.id,` · 当前`,pt[t.goal.state],` ·`,` `,(0,x.jsx)(N,{value:t.created_at}),!t.current&&` · 已不是当前状态`]}),(0,x.jsx)(`p`,{className:`goal-content`,children:t.goal.content}),(0,x.jsx)(Mt,{messages:t.evidence_messages}),(0,x.jsxs)(`div`,{className:`actions`,children:[(0,x.jsxs)(`button`,{className:`text-button`,onClick:()=>e(t.goal.id),children:[`查看目标 #`,t.goal.id]}),t.reopenable&&(0,x.jsxs)(`button`,{onClick:()=>{b(``),c(t.goal)},children:[`重开目标 #`,t.goal.id]})]})]},`r${t.id}`):(0,x.jsxs)(`article`,{className:`goal-operation`,children:[(0,x.jsxs)(`p`,{children:[(0,x.jsx)(M,{children:At[t.outcome]}),` 目标 #`,t.goal_a,` 与 #`,t.goal_b,` ·`,` `,Et(t.reason),` ·`,` `,(0,x.jsx)(N,{value:t.created_at})]}),t.goals.map(e=>(0,x.jsxs)(`p`,{className:`goal-content`,children:[`#`,e.id,`:`,e.content]},e.id)),(0,x.jsxs)(`div`,{className:`actions`,children:[t.goals.map(t=>(0,x.jsxs)(`button`,{className:`text-button`,onClick:()=>e(t.id),children:[`查看目标 #`,t.id]},t.id)),t.undoable&&(0,x.jsxs)(`button`,{onClick:()=>{b(``),d(t)},children:[`撤销合并 #`,t.id]}),t.undone_at&&(0,x.jsx)(`span`,{className:`muted`,children:`已撤销`})]})]},`d${t.id}`)),o.data&&(0,x.jsx)(T,{total:o.data.total,limit:10,offset:i,change:a})]}),u&&(0,x.jsx)(jt,{judgment:u,busy:p,error:h,confirm:()=>void E(),cancel:()=>d(null)}),s&&(0,x.jsx)(Pt,{goal:s,busy:p,error:h,confirm:()=>void w(),cancel:()=>c(null)})]})}var It=e=>{let t=new Date(e);if(!Number.isFinite(t.getTime()))return``;let n=e=>String(e).padStart(2,`0`);return`${t.getFullYear()}-${n(t.getMonth()+1)}-${n(t.getDate())}T${n(t.getHours())}:${n(t.getMinutes())}:${n(t.getSeconds())}`};function Lt({goal:e,catalog:t,busy:n,locked:r=!1,submit:i,cancel:a}){let[o,s]=(0,l.useState)(e?.content||``),[c,u]=(0,l.useState)(e?.kind||`normal`),[d,f]=(0,l.useState)(e?`keep`:`none`),[p,m]=(0,l.useState)(``),[h,g]=(0,l.useState)(e?.deadline&&!e.deadline_unresolved?It(e.deadline):``),[_,v]=(0,l.useState)(e?.reminder_minutes?.toString()??``),[y,b]=(0,l.useState)([]),[S,C]=(0,l.useState)(``),[w,T]=(0,l.useState)(``);function E(t){if(t.preventDefault(),n||r)return;if(T(``),!o.trim()||o.trim().length>4e3){T(`目标正文须为 1—4000 个字符且不为空白`);return}let a={content:o.trim()};if(c===`normal`){if(_.trim()&&(!Number.isInteger(Number(_))||Number(_)<0||Number(_)>525600)){T(`提醒提前量须为 0—525600 的整数,留空使用默认值`);return}if((!e||_!==(e.reminder_minutes?.toString()??``))&&(a.reminder_minutes=_.trim()?Number(_):null),d===`date`){if(!/^\d{4}-\d{2}-\d{2}$/.test(p)||!Number.isFinite(Date.parse(p))||new Date(p).toISOString().slice(0,10)!==p){T(`请选择有效的截止日期`);return}a.deadline=p}else if(d===`datetime`){let e=new Date(h);if(!h||!Number.isFinite(e.getTime())||It(e.toISOString())!==(h.length===16?h+`:00`:h)){T(`请选择有效的截止时间`);return}a.deadline=e.toISOString()}else d===`none`&&(a.deadline=null)}else e||(a.deadline=null,a.reminder_minutes=null);if(!e){if(y.length>100){T(`涉及的人最多 100 位`);return}Object.assign(a,{kind:c,people:y,entry_id:S||null})}i(a)}return(0,x.jsxs)(`form`,{className:`goal-form`,onSubmit:E,noValidate:!0,children:[w&&(0,x.jsx)(A,{error:!0,children:w}),(0,x.jsxs)(`fieldset`,{className:`lifecycle-fieldset`,disabled:n||r,children:[!e&&(0,x.jsxs)(`label`,{children:[`创建类型`,(0,x.jsxs)(`select`,{value:c,onChange:e=>u(e.target.value),children:[(0,x.jsx)(`option`,{value:`normal`,children:`普通目标`}),(0,x.jsx)(`option`,{value:`question`,children:`询问`})]})]}),(0,x.jsxs)(`label`,{children:[`目标正文`,(0,x.jsx)(`textarea`,{required:!0,maxLength:4e3,rows:4,value:o,onChange:e=>s(e.target.value)})]}),c===`question`?(0,x.jsx)(`p`,{className:`lifecycle-help`,children:`询问没有截止时间和提醒,由宿主决定何时询问。`}):(0,x.jsxs)(x.Fragment,{children:[e&&(0,x.jsxs)(`p`,{children:[`当前截止时间:`,(0,x.jsx)(St,{goal:e})]}),(0,x.jsxs)(`div`,{className:`form-grid`,children:[(0,x.jsxs)(`label`,{children:[`截止方式`,(0,x.jsxs)(`select`,{value:d,onChange:e=>f(e.target.value),children:[e&&(0,x.jsx)(`option`,{value:`keep`,children:`保持当前截止时间`}),(0,x.jsx)(`option`,{value:`none`,children:`不设截止时间`}),(0,x.jsx)(`option`,{value:`date`,children:`指定日期`}),(0,x.jsx)(`option`,{value:`datetime`,children:`指定日期和时间`})]})]}),(0,x.jsxs)(`label`,{children:[`提醒提前量(分钟)`,(0,x.jsx)(`input`,{type:`number`,min:0,max:525600,step:1,value:_,placeholder:`留空使用默认值`,onChange:e=>v(e.target.value)})]}),d===`date`&&(0,x.jsxs)(`label`,{children:[`截止日期`,(0,x.jsx)(`input`,{"aria-label":`截止日期`,type:`date`,required:!0,value:p,onChange:e=>m(e.target.value)}),(0,x.jsx)(`small`,{children:`按角色时区当天 23:59:59 截止。`})]}),d===`datetime`&&(0,x.jsxs)(`label`,{children:[`截止日期和时间`,(0,x.jsx)(`input`,{"aria-label":`截止日期和时间`,type:`datetime-local`,step:1,required:!0,value:h,onChange:e=>g(e.target.value)}),(0,x.jsxs)(`small`,{children:[`按本机时区`,` `,Intl.DateTimeFormat().resolvedOptions().timeZone,` `,`输入;保存后按角色时区显示。`]})]})]}),(0,x.jsx)(`p`,{className:`lifecycle-help`,children:`留空提前量使用默认值;0 表示到期时提醒。修改期限或提前量会更新尚未取走的提醒,已取走的记录保留。`})]}),!e&&(0,x.jsxs)(`div`,{className:`form-grid`,children:[(0,x.jsxs)(`label`,{children:[`涉及的人`,(0,x.jsx)(`select`,{"aria-label":`涉及的人`,multiple:!0,size:4,value:y,onChange:e=>b(Array.from(e.target.selectedOptions,e=>e.value)),children:t?.people.filter(e=>![`self`,`scene`].includes(e.id)&&![`self`,`scene`].includes(e.kind||``)).map(e=>(0,x.jsx)(`option`,{value:e.id,children:yt(e.id,t)},e.id))}),(0,x.jsx)(`small`,{children:`可多选,也可不指定。`})]}),(0,x.jsxs)(`label`,{children:[`记录产生入口`,(0,x.jsxs)(`select`,{"aria-label":`记录产生入口`,value:S,onChange:e=>C(e.target.value),children:[(0,x.jsx)(`option`,{value:``,children:`不指定`}),t?.entries.map(e=>(0,x.jsxs)(`option`,{value:e.id,children:[e.name,` · `,e.id]},e.id))]}),(0,x.jsx)(`small`,{children:`仅记录产生位置,所有入口共享此目标。`})]})]}),!e&&(0,x.jsx)(`p`,{className:`lifecycle-help`,children:`管理员创建的目标独立保留;发现相似项只标“可能重复”,由管理员明确合并或驳回。`}),(0,x.jsx)(`div`,{className:`actions`,children:(0,x.jsx)(`button`,{className:`primary`,children:n?`正在保存…`:e?`保存目标`:`创建`})})]}),(0,x.jsx)(`button`,{type:`button`,className:`secondary`,disabled:n,onClick:a,children:`取消`})]})}function Rt({note:e,openGoal:t}){return(0,x.jsxs)(`article`,{className:`notification-card`,children:[(0,x.jsxs)(`div`,{className:`panel-heading`,children:[(0,x.jsx)(`h3`,{children:gt[e.reminder_kind]}),(0,x.jsx)(M,{tone:e.status===`pending`?`purple`:``,children:_t[e.status]})]}),(0,x.jsx)(`p`,{className:`goal-content`,children:e.content}),(0,x.jsxs)(`div`,{className:`goal-time-lines`,children:[(0,x.jsxs)(`p`,{children:[`发布时间:`,(0,x.jsx)(N,{value:e.published_at})]}),(0,x.jsxs)(`p`,{children:[`计划时间:`,(0,x.jsx)(N,{value:e.scheduled_at})]}),e.deadline_at&&(0,x.jsxs)(`p`,{children:[`发布时的截止时间:`,(0,x.jsx)(N,{value:e.deadline_at})]}),e.taken_at&&(0,x.jsxs)(`p`,{children:[`取走时间:`,(0,x.jsx)(N,{value:e.taken_at})]}),e.cancelled_at&&(0,x.jsxs)(`p`,{children:[`取消时间:`,(0,x.jsx)(N,{value:e.cancelled_at})]})]}),(0,x.jsxs)(`button`,{className:`text-button`,onClick:()=>t(e.goal_id),children:[`查看目标 #`,e.goal_id]})]})}function zt({goalId:e,openGoal:t}){let[n,r]=(0,l.useState)(e?``:`pending`),[i,a]=(0,l.useState)(0),o=new URLSearchParams({limit:`30`,offset:String(i)});n&&o.set(`status`,n),e&&o.set(`goal_id`,String(e));let s=y(`/notifications?${o}`,i===0?2e3:0);return(0,x.jsxs)(`section`,{className:`panel goals-panel`,"aria-label":`提醒列表`,children:[(0,x.jsxs)(`div`,{className:`panel-heading`,children:[(0,x.jsx)(`h2`,{children:e?`该目标的提醒`:`提醒`}),(0,x.jsx)(`button`,{className:`text-button`,onClick:s.refresh,children:`刷新提醒`})]}),(0,x.jsx)(`p`,{className:`goal-help`,children:`“已取走”只表示宿主拿到了提醒,不代表已送达用户或目标完成。此处查看不会取走提醒。`}),(0,x.jsxs)(`label`,{className:`notification-filter`,children:[`提醒状态`,(0,x.jsxs)(`select`,{value:n,onChange:e=>{r(e.target.value),a(0)},children:[(0,x.jsx)(`option`,{value:``,children:`全部`}),Object.entries(_t).map(([e,t])=>(0,x.jsx)(`option`,{value:e,children:t},e))]})]}),s.error&&(0,x.jsxs)(A,{error:!0,children:[s.error,`。`,s.data&&`以下为上次读取结果。`]}),!s.data&&!s.error&&(0,x.jsx)(`p`,{role:`status`,children:`正在读取提醒…`}),s.data?.items.length===0&&(0,x.jsx)(j,{title:`当前范围没有提醒`,children:`有截止时间的普通目标会在临近、到期时产生提醒;询问不安排提醒。`}),s.data?.items.map(e=>(0,x.jsx)(Rt,{note:e,openGoal:t},e.id)),s.data&&(0,x.jsx)(T,{total:s.data.total,offset:i,change:a})]})}var Bt=`合并待核对(双方互有细节,不代表事实矛盾)`;function W({details:e,ids:t,openMemory:n,disabled:r=!1}){return(0,x.jsxs)(`div`,{className:`merge-detail-review`,children:[(0,x.jsx)(`p`,{children:`双方各有另一条没有的细节,未自动合并。采纳只确认建议,不执行合并,也不改正文和相信程度。`}),[`a`,`b`].map((i,a)=>(0,x.jsxs)(`div`,{children:[t[a]==null?(0,x.jsxs)(`strong`,{children:[`第 `,a+1,` 条记忆的独有细节`]}):(0,x.jsxs)(`button`,{disabled:r,className:`text-button`,onClick:()=>n(t[a]),children:[`记忆 #`,t[a],` 的独有细节`]}),e?.[i]?.length?(0,x.jsx)(`ul`,{children:e[i].map((e,t)=>(0,x.jsx)(`li`,{children:e},t))}):(0,x.jsx)(`p`,{className:`muted`,children:`未保存独有细节,请核对报告正文。`})]},i))]})}var Vt={relationship:`与我的关系`,fact:`稳定事实`,preference:`偏好与习惯`,recent:`近期重要事项`},Ht={published:`已发布`,candidate:`待确认`,rejected:`已拒绝`,failed:`失败`,skipped:`已跳过`,conflict:`版本或依据冲突`,running:`生成中`};function Ut(e){return{memory_purged:`依据记忆已彻底清除`,memory_unavailable:`依据记忆已遗忘或删除`,visibility_changed:`依据不再满足全部入口共享条件`,no_longer_related:`依据不再与此人物相关`,memory_changed:`依据记忆已修订`,subject_changed:`人物资料或关联发生变化`,subject_merged:`人物已合并`,subject_unavailable:`此人物当前不可生成摘要`,subject_kind:`此类主体不生成人物摘要`,time_range_expired:`近期事项已到期`,purged:`因彻底清除而清空`,disabled:`人物摘要开关已关闭`,insufficient_memories:`相关全局有效记忆不足(至少需要 8 条)`,no_changes_since_attempt:`上次被拒或确定性失败后,尚无新依据`,not_due:`尚未达到更新条件`,generation_in_progress:`管理员生成中,本轮已跳过`,interrupted:`任务被中断,可以重试`,invalid_output:`模型输出格式无效`,invalid_check_output:`模型检查格式无效`,invalid_check_item:`逐条检查格式无效`,too_many_deleted:`检查累计删去超过一半的模型条目`,empty:`没有可保留的摘要内容`,too_many_items:`条目数超出预算`,too_long:`摘要字数超出预算`,token_budget:`摘要 token 超出预算`,invalid_item_or_references:`条目或引用依据无效`,missing_time_range:`近期事项缺少时间范围`,invalid_or_expired_time_range:`时间范围无效或已经到期`,unexpected_time_range:`非近期事项带有不适用的时间范围`,too_many_relationships:`与我的关系最多保留一条`,timeout:`模型请求超时,可以重试`,retryable:`模型暂时失败,可以重试`,temporary_unavailable:`模型暂时不可用,可以稍后重试`,paused:`模型用途已暂停`,usage_limit:`已达到每日用量上限`,call_budget:`剩余模型调用预算不足`,unconfigured:`模型尚未配置`,configuration:`模型配置无效`,account:`模型账户不可用`,refused:`模型拒绝处理`,unexpected_error:`生成过程中发生错误,可以重试`,superseded:`已被新候选或编辑取代`,administrator_rejected:`管理员拒绝`,evidence_changed:`依据已变化,请刷新后重试`,current_version_changed:`当前版本已变化,请刷新后重试`,settings_or_subject_changed:`设置或人物资料已变化,请刷新后重试`,settings_changed:`设置已变化,请刷新后重试`,candidate_changed:`候选已变化,请刷新后重试`}[e]||e}var Wt=(e,t)=>`#/people?id=${encodeURIComponent(e)}${t?`&summary_version=${t}`:``}`;function Gt({basis:e,catalog:t,openMemory:n}){let r=y(e.memory?`/memories/${e.memory_id}`:null,5e3),i=r.data,a=[...new Set([i?.entry_id,...(i?.sources||[]).map(e=>e.message?.entry_id)].filter(e=>!!e))];return(0,x.jsxs)(`article`,{className:`persona-evidence`,children:[(0,x.jsxs)(`h4`,{children:[`记忆 #`,e.memory_id]}),(0,x.jsxs)(`p`,{className:`muted`,children:[`记录修订 `,e.revision,` ·`,` `,e.memory?`当前修订 ${e.memory.revision} · ${E(e.memory.lifecycle)}`:`当前记忆不可用`]}),(0,x.jsx)(`p`,{className:`persona-content`,children:e.content_at_revision??`该修订正文不可用`}),i&&(0,x.jsxs)(x.Fragment,{children:[(0,x.jsxs)(`p`,{className:`muted`,children:[`当前记忆记录时间:`,(0,x.jsx)(N,{value:i.created_at}),`;事件时间:`,(0,x.jsx)(N,{value:i.event_time??null})]}),(0,x.jsxs)(`p`,{className:`muted`,children:[`当前来源入口类型:`,a.length?a.map(e=>{let n=t?.entries.find(t=>t.id===e);return n?`${{group:`群聊`,private:`私聊`}[n.kind]||n.kind} · ${n.name}`:`入口 ${e}(类型暂不可用)`}).join(`;`):i.sources.some(e=>e.kind===`initial_setting`)?`初始设定`:`未记录入口`]}),(0,x.jsx)(`small`,{children:`以上时间和入口来自当前记忆详情,不是该版本生成时的历史快照。`})]}),r.loading&&(0,x.jsx)(`p`,{children:`正在读取当前记忆元数据…`}),r.error&&(0,x.jsxs)(A,{error:!0,children:[r.error,(0,x.jsx)(`button`,{onClick:r.refresh,children:`重试记忆元数据`})]}),(0,x.jsxs)(`button`,{className:`text-button`,disabled:!e.memory,onClick:()=>n(e.memory_id),children:[`查看记忆 #`,e.memory_id]})]})}function Kt({sentence:e,index:t,catalog:n,openMemory:r}){let[i,a]=(0,l.useState)(!1),o=e.admin_written||e.origin===`admin`;return(0,x.jsxs)(`li`,{className:`persona-sentence`,children:[(0,x.jsxs)(`div`,{className:`persona-sentence-line`,children:[(0,x.jsxs)(`span`,{className:`muted`,children:[t+1,`.`]}),(0,x.jsx)(`p`,{children:e.text}),(0,x.jsx)(M,{tone:o?`warning`:``,children:o?`手写`:re[e.origin]||e.origin})]}),(0,x.jsxs)(`button`,{className:`text-button`,"aria-expanded":i,onClick:()=>a(!i),children:[i?`收起`:`展开`,`第 `,t+1,` 句依据`]}),i&&(0,x.jsxs)(`div`,{className:`persona-basis`,children:[(0,x.jsxs)(`p`,{children:[`该句的依据日期:`,e.dates.length?e.dates.join(`、`):`未记录日期`,`(`,e.date_count,` 个日期)`,e.initial_setting&&` · 含初始设定`]}),e.basis.map(e=>(0,x.jsx)(Gt,{basis:e,catalog:n,openMemory:r},`${e.memory_id}-${e.revision}`)),!e.basis.length&&(0,x.jsx)(`p`,{className:`muted`,children:o?`管理员手写内容,没有记忆依据;手写标记不代表模型检查通过。`:`初始模板,没有逐句记忆依据。`})]})]})}var qt=e=>typeof e==`object`&&e&&!Array.isArray(e)?e:null;function Jt({value:e,index:t}){let n=qt(e);if(!n)return(0,x.jsxs)(`li`,{children:[`第 `,t+1,` 句:检查结果格式不完整`]});let r=(e,t,n)=>typeof e==`boolean`?e?t:n:`未报告`;return(0,x.jsxs)(`li`,{children:[`第 `,typeof n.index==`number`?n.index:t+1,` `,`句:依据支持 `,r(n.supported,`是`,`否`),`;虚构`,` `,r(n.fabricated,`是`,`否`),`;场景限定`,` `,r(n.scene_qualified,`是`,`否`),typeof n.reason==`string`&&(0,x.jsx)(`p`,{children:n.reason}),Array.isArray(n.violations)&&n.violations.some(e=>typeof e==`string`)&&(0,x.jsxs)(`p`,{children:[`违反要求:`,n.violations.filter(e=>typeof e==`string`).join(`;`)]})]})}function Yt({version:e}){let{checks:t}=e,n=qt(t.model);return(0,x.jsxs)(`div`,{className:`persona-checks`,children:[(0,x.jsx)(`h3`,{children:`检查结果`}),(0,x.jsxs)(`p`,{children:[t.passed===!0?`综合检查通过`:t.passed===!1?`综合检查未通过`:`没有综合检查结论`,t.administrator_published&&` · 管理员直接发布`]}),(0,x.jsx)(`p`,{className:`muted`,children:`手写标记表示内容的来源,不代表模型检查通过。`}),(0,x.jsx)(`h4`,{children:`确定性检查`}),(0,x.jsx)(`p`,{children:t.deterministic?.passed===!0?`通过`:t.deterministic?.passed===!1?`未通过`:`没有确定性检查记录`}),!!t.deterministic?.errors?.length&&(0,x.jsx)(`ul`,{children:t.deterministic.errors.map((e,t)=>(0,x.jsx)(`li`,{children:ae(e)},t))}),!!t.deterministic?.warnings?.length&&(0,x.jsx)(`ul`,{className:`muted`,children:t.deterministic.warnings.map((e,t)=>(0,x.jsxs)(`li`,{children:[`提示:`,ae(e)]},t))}),(0,x.jsx)(`h4`,{children:`模型检查`}),t.model==null?(0,x.jsx)(`p`,{children:`未进行模型检查`}):n?(0,x.jsxs)(x.Fragment,{children:[typeof n.change_degree==`string`&&(0,x.jsxs)(`p`,{children:[`判断的变化程度:`,R[n.change_degree]||n.change_degree]}),typeof n.reason==`string`&&(0,x.jsx)(`p`,{children:n.reason}),Array.isArray(n.sentences)?(0,x.jsx)(`ul`,{children:n.sentences.map((e,t)=>(0,x.jsx)(Jt,{value:e,index:t},t))}):(0,x.jsx)(`p`,{children:`逐句检查结果格式不完整`})]}):(0,x.jsx)(`p`,{children:`模型检查结果格式不完整`}),!!t.model_errors?.length&&(0,x.jsx)(`ul`,{children:t.model_errors.map((e,t)=>(0,x.jsx)(`li`,{children:ae(e)},t))}),t.admin_content_removed_or_changed&&(0,x.jsx)(A,{children:`候选删去或改动了手写内容,变化程度按“大”处理。默认发布方式需要确认;“全部自动”模式会直接发布检查通过的候选。`})]})}function Xt({id:e,title:t,catalog:n,openMemory:r,onAction:i,locked:a=!1,currentID:o,history:s=!1}){let c=y(`/persona/versions/${e}`,5e3),l=c.data;return(0,x.jsxs)(`section`,{className:`panel persona-version`,"aria-label":t,children:[l&&(0,x.jsxs)(`h2`,{children:[t,` · v`,e]}),c.error&&(0,x.jsxs)(A,{error:!0,children:[c.error,(0,x.jsx)(`button`,{onClick:c.refresh,children:`重试版本详情`})]}),l?(0,x.jsxs)(x.Fragment,{children:[(0,x.jsxs)(`div`,{className:`persona-meta`,children:[(0,x.jsx)(M,{tone:l.status===`pending`||l.status===`rejected`?`warning`:``,children:ne[l.status]}),(0,x.jsx)(`span`,{children:L[l.source]||l.source}),(0,x.jsxs)(`span`,{children:[`变化程度:`,l.change_degree?R[l.change_degree]||l.change_degree:`未记录`]})]}),(0,x.jsxs)(`p`,{className:`muted`,children:[`生成时间:`,(0,x.jsx)(N,{value:l.generated_at}),` · 发布时间:`,(0,x.jsx)(N,{value:l.published_at})]}),(0,x.jsxs)(`p`,{className:`muted`,children:[l.base_version_id?`基于 v${l.base_version_id}`:`初始版本`,l.rollback_of&&` · 回滚自 v${l.rollback_of}`]}),(0,x.jsx)(`ol`,{className:`persona-sentences`,children:l.sentences.map((e,t)=>(0,x.jsx)(Kt,{sentence:e,index:t,catalog:n,openMemory:r},`${t}-${e.text}`))}),!l.sentences.length&&(0,x.jsx)(`p`,{className:`persona-content`,children:l.content}),!!l.rejection_reasons.length&&(0,x.jsxs)(`div`,{className:`notice error`,children:[(0,x.jsx)(`h3`,{children:`拒绝原因`}),(0,x.jsx)(`ul`,{children:l.rejection_reasons.map((e,t)=>(0,x.jsx)(`li`,{children:ae(e)},t))})]}),(0,x.jsx)(Yt,{version:l}),l.settings&&(0,x.jsxs)(`details`,{children:[(0,x.jsx)(`summary`,{children:`本次候选接受时的设置`}),(0,x.jsxs)(`p`,{children:[`发布方式:`,{small_medium_auto:`小或中自动发布`,all_auto:`全部自动`,all_manual:`全部人工确认`}[l.settings.publish_mode]||l.settings.publish_mode]}),(0,x.jsx)(`h4`,{children:`生成目标`}),(0,x.jsx)(`p`,{className:`persona-content`,children:l.settings.goal}),(0,x.jsx)(`h4`,{children:`监管要求`}),(0,x.jsx)(`p`,{className:`persona-content`,children:l.settings.rules})]}),(0,x.jsxs)(`div`,{className:`persona-actions`,children:[i&&o&&l.status===`pending`&&(0,x.jsxs)(x.Fragment,{children:[(0,x.jsx)(`button`,{className:`primary`,disabled:a||!!c.error,onClick:()=>i(`confirm`,l),children:`确认发布候选`}),(0,x.jsx)(`button`,{disabled:a||!!c.error,onClick:()=>i(`reject`,l),children:`拒绝候选`})]}),i&&o&&s&&[`current`,`history`].includes(l.status)&&(0,x.jsx)(`button`,{disabled:a||!!c.error,onClick:()=>i(`rollback`,l),children:`回滚到此版本`}),(0,x.jsx)(`a`,{href:`#/operations?object_type=persona&object_id=${e}`,children:`查看版本操作记录`})]})]}):(0,x.jsx)(`p`,{children:`正在读取版本详情…`})]})}function Zt({sentences:e,start:t}){return e.length?(0,x.jsx)(`ol`,{className:`persona-diff-sentences`,start:t,children:e.map((e,t)=>(0,x.jsxs)(`li`,{children:[(0,x.jsx)(`p`,{children:e.text}),(0,x.jsxs)(`small`,{children:[re[e.origin]||e.origin,` · 依据:`,e.basis.length?e.basis.map(e=>`#${e.memory_id} 修订 ${e.revision}`).join(`、`):`无`,` `,`· 日期:`,e.dates.join(`、`)||`未记录`,`(`,e.date_count,` 个日期)`,e.initial_setting&&` · 初始设定`]})]},t))}):(0,x.jsx)(`p`,{className:`muted`,children:`无句子`})}function Qt({before:e,after:t}){let n=y(`/persona/diff?before_version=${e}&after_version=${t}`);return(0,x.jsxs)(`section`,{className:`persona-diff`,"aria-label":`版本差异 v${e} 到 v${t}`,children:[(0,x.jsxs)(`h3`,{children:[`逐句差异:v`,e,` → v`,t]}),n.error&&(0,x.jsxs)(A,{error:!0,children:[n.error,(0,x.jsx)(`button`,{onClick:n.refresh,children:`重试版本对比`})]}),!n.data&&!n.error&&(0,x.jsx)(`p`,{children:`正在读取差异…`}),n.data?.changes.length===0&&(0,x.jsx)(`p`,{children:`这两个版本的句子与依据没有差异。`}),n.data?.changes.map((n,r)=>(0,x.jsxs)(`article`,{className:`persona-change persona-change-${n.kind}`,children:[(0,x.jsx)(`h4`,{children:{insert:`新增句子`,delete:`删除句子`,replace:`改写句子`,basis_changed:`正文相同,依据或归属变化`}[n.kind]}),(0,x.jsxs)(`div`,{className:`persona-diff-columns`,children:[(0,x.jsxs)(`div`,{children:[(0,x.jsxs)(`strong`,{children:[`之前 · v`,e]}),(0,x.jsx)(Zt,{sentences:n.before,start:n.before_start})]}),(0,x.jsxs)(`div`,{children:[(0,x.jsxs)(`strong`,{children:[`之后 · v`,t]}),(0,x.jsx)(Zt,{sentences:n.after,start:n.after_start})]})]})]},r))]})}function $t({value:e,openMemory:t}){return(0,x.jsxs)(`details`,{className:`source consolidation-excerpts`,children:[(0,x.jsx)(`summary`,{children:`查看来源摘录`}),(0,x.jsx)(`p`,{className:`fine-print`,children:`本次模型判断时的来源摘录;完整来源与前后文可在记忆详情查看。`}),!e?.length&&(0,x.jsx)(`p`,{children:`没有保存的来源摘录。`}),e?.map((e,n)=>(0,x.jsxs)(`div`,{children:[(0,x.jsxs)(`button`,{className:`text-button`,onClick:()=>t(e.memory_id),children:[`记忆 #`,e.memory_id]}),!e.sources.length&&(0,x.jsx)(`p`,{className:`quiet`,children:`没有可用的来源消息摘录。`}),e.sources.map(e=>(0,x.jsxs)(`article`,{className:`consolidation-excerpt`,children:[(0,x.jsxs)(`p`,{className:`muted`,children:[`消息 #`,e.id,` · `,e.entry_name||e.entry_id,` ·`,` `,{private:`私聊`,group:`群聊`,trial:`试用`}[e.entry_kind||``]||e.entry_kind,` `,`· `,e.sender_name||e.sender,` ·`,` `,(0,x.jsx)(N,{value:e.at||e.occurred_at||null})]}),(0,x.jsx)(`p`,{className:`fine-print`,children:{message:`他人消息`,self_output:`角色输出`,action_result:`行动结果`,event:`事件`}[e.kind]||e.kind}),(0,x.jsx)(`blockquote`,{children:e.text}),e.quote_content&&(0,x.jsxs)(`div`,{className:`source-quote`,children:[(0,x.jsxs)(`small`,{children:[`原消息引用 `,e.quote_author||``]}),(0,x.jsx)(`blockquote`,{children:e.quote_content})]})]},e.id))]},`${e.memory_id}:${n}`))]})}function en(e){let t=e.details;return e.outcome===`auto_conflict`?z[t.relation||``]||t.relation:e.outcome===`auto_dependency`?B[t.verdict||``]||t.verdict:e.outcome===`protected`?pe(`protected`):`未合并:${pe(e.reason||``)}`}function tn({runId:e,item:t,refresh:n}){let r=t.details.undo,[i,a]=(0,l.useState)(!1),[o,s]=(0,l.useState)(``);if(!r)return null;if(r.undone_at)return(0,x.jsxs)(`p`,{className:`muted`,children:[`已撤销 · `,(0,x.jsx)(N,{value:r.undone_at})]});async function c(){if(!i){a(!0),s(``);try{await g(`/maintenance/${e}/consolidation/${t.item_key}/undo`,_(`POST`,{expected_revision:r.expected_revision})),n?.()}catch(e){s(v(e))}finally{a(!1)}}}return(0,x.jsxs)(`div`,{className:`actions`,children:[(0,x.jsx)(`button`,{className:`text-button`,disabled:i,onClick:c,children:`撤销此次处理`}),o&&(0,x.jsx)(A,{error:!0,children:o})]})}function nn({name:e,items:t,openMemory:n,runId:r,refresh:i}){let[a,o]=(0,l.useState)(0);return(0,x.jsxs)(`section`,{className:`detail-section`,"aria-label":e,children:[(0,x.jsxs)(`h3`,{children:[e,` `,(0,x.jsx)(M,{children:t.length})]}),e.includes(`建议`)&&(0,x.jsx)(`p`,{className:`fine-print`,children:`仅管理员可见。采纳只记录确认,不改正文和相信程度;已确认的建议也不进入宿主召回。`}),!t.length&&(0,x.jsx)(`p`,{className:`quiet`,children:`本次暂无记录。`}),t.slice(a,a+30).map(e=>{let t=e.details;return(0,x.jsxs)(`article`,{className:`maintenance-item`,children:[e.outcome===`merged`?(0,x.jsxs)(x.Fragment,{children:[(0,x.jsxs)(`div`,{className:`actions`,children:[t.absorbed_ids?.map(e=>(0,x.jsxs)(`button`,{className:`text-button`,onClick:()=>n(e),children:[`记忆 #`,e]},e)),(0,x.jsx)(`span`,{children:`并入`}),t.result_id!=null&&(0,x.jsxs)(`button`,{className:`text-button`,onClick:()=>n(t.result_id),children:[`记忆 #`,t.result_id]})]}),(0,x.jsx)(`p`,{children:`全部来源已归入保留记忆;被合并的记忆保留为占位。`}),(0,x.jsxs)(`p`,{children:[`来源消息:`,t.source_messages?.map(e=>`#${e}`).join(`、`)||`无消息来源`,` `,`· 重定向派生依据 `,t.redirected_dependencies??0,` 处`]})]}):t.auto_resolution===`auto_v1`?(0,x.jsxs)(x.Fragment,{children:[(0,x.jsx)(M,{tone:`warning`,children:`自主处理`}),` `,(0,x.jsx)(`span`,{children:en(e)}),t.check_failure&&(0,x.jsxs)(`p`,{className:`muted`,children:[`核对未通过:`,t.check_failure]}),t.suggestions?.map(e=>(0,x.jsxs)(`p`,{children:[(0,x.jsx)(M,{children:ce[e.kind]||e.kind}),` `,(0,x.jsx)(M,{children:le[e.review_status]}),` `,e.text]},e.id))]}):(0,x.jsxs)(x.Fragment,{children:[(0,x.jsx)(M,{tone:`warning`,children:`模型建议`}),` `,(0,x.jsx)(`span`,{children:t.suggestion_type===`merge_details`?Bt:e.outcome===`conflicts`?`矛盾检查`:`依赖复核`}),t.suggestion_type===`merge_details`&&(0,x.jsx)(W,{details:t.unique_details,ids:t.before_memories?.slice(0,2).map(e=>e.id)||[],openMemory:n}),t.suggestions?.map(e=>(0,x.jsxs)(`div`,{className:`suggestion-summary`,children:[(0,x.jsxs)(`p`,{children:[(0,x.jsx)(M,{children:t.suggestion_type===`merge_details`?`合并待核对`:ce[e.kind]||e.kind}),` `,(0,x.jsx)(M,{children:le[e.review_status]}),` 建议 #`,e.id]}),(0,x.jsx)(`p`,{children:e.text}),e.memory_id!=null&&(0,x.jsxs)(`button`,{className:`text-button`,onClick:()=>n(e.memory_id),children:[`查看记忆 #`,e.memory_id,` 的建议`]}),e.confirmed_at&&(0,x.jsxs)(`p`,{className:`muted`,children:[`管理员确认:`,(0,x.jsx)(N,{value:e.confirmed_at})]}),e.cleared_at&&(0,x.jsxs)(`p`,{className:`muted`,children:[`管理员清除:`,(0,x.jsx)(N,{value:e.cleared_at})]})]},e.id)),!t.suggestions?.length&&e.memory_id!=null&&(0,x.jsxs)(`button`,{className:`text-button`,onClick:()=>n(e.memory_id),children:[`查看记忆 #`,e.memory_id]})]}),t.report&&(0,x.jsx)(`p`,{className:`report-text`,children:t.report}),!!t.before_memories?.length&&(0,x.jsxs)(`details`,{className:`source`,children:[(0,x.jsx)(`summary`,{children:`涉及的记忆与修订快照`}),t.before_memories.map(e=>{let r=t.after_memories?.find(t=>t.id===e.id);return(0,x.jsxs)(`div`,{children:[(0,x.jsxs)(`button`,{className:`text-button`,onClick:()=>n(e.id),children:[`记忆 #`,e.id]}),(0,x.jsxs)(`span`,{children:[` `,`· 修订 `,e.revision,r&&` → ${r.revision}`]}),(0,x.jsx)(`p`,{className:`report-text`,children:e.content||`正文已不可用`}),r?.content!==e.content&&(0,x.jsxs)(`p`,{className:`report-text`,children:[`整理后:`,r?.content||`正文已不可用`]})]},e.id)})]}),(0,x.jsx)(tn,{runId:r,item:e,refresh:i}),(0,x.jsx)($t,{value:t.source_excerpts,openMemory:n}),(0,x.jsx)(`small`,{children:(0,x.jsx)(N,{value:e.created_at})})]},`${e.phase}:${e.item_key}`)}),(0,x.jsx)(T,{total:t.length,offset:a,change:o})]})}function rn({data:e}){let[t,n]=(0,l.useState)(0),r=e.items.filter(e=>e.outcome===`goals_reviewed`);return!e.goal_review&&!r.length?null:(0,x.jsxs)(`section`,{className:`detail-section`,"aria-label":`目标复核结果`,children:[(0,x.jsx)(`h3`,{children:`目标复核结果`}),(0,x.jsx)(`p`,{children:e.goal_review?.enabled===!1?`未执行:${pe(e.goal_review.skip_reason||`disabled`)}`:`已检查 ${e.goal_review?.checked??0} 个目标,${r.length} 个目标的依据标注发生变化。`}),(0,x.jsx)(`p`,{className:`fine-print`,children:`复核只更新依据标注,不自动完成或放弃目标,也不改变提醒。`}),r.slice(t,t+30).map(e=>(0,x.jsxs)(`article`,{className:`maintenance-item`,children:[(0,x.jsxs)(`a`,{href:`#/state?tab=goals&id=${e.object_id}`,children:[`查看目标 #`,e.object_id]}),(0,x.jsxs)(`p`,{children:[`新增或重新启用标注:`,e.details.annotation_ids?.map(e=>`#${e}`).join(`、`)||`无`]}),(0,x.jsxs)(`p`,{children:[`结束的旧标注:`,e.details.ended_annotation_ids?.map(e=>`#${e}`).join(`、`)||`无`]}),(0,x.jsx)(`small`,{children:(0,x.jsx)(N,{value:e.created_at})})]},e.item_key)),(0,x.jsx)(T,{total:r.length,offset:t,change:n})]})}function an({data:e}){let[t,n]=(0,l.useState)(0),r=e.items.filter(e=>e.outcome===`person_summary`);return!r.length&&e.consolidation?.settings.person_summary_enabled==null?null:(0,x.jsxs)(`section`,{className:`detail-section`,"aria-label":`人物摘要处理结果`,children:[(0,x.jsxs)(`h3`,{children:[`人物摘要处理结果 `,(0,x.jsx)(M,{children:r.length})]}),e.consolidation?.settings.person_summary_enabled===!1&&(0,x.jsx)(`p`,{children:`本次运行已关闭人物摘要更新。`}),!r.length&&(0,x.jsx)(`p`,{children:`本次没有人物摘要处理记录。`}),r.slice(t,t+30).map(e=>(0,x.jsxs)(`article`,{className:`maintenance-item`,children:[(0,x.jsx)(`p`,{children:(0,x.jsx)(M,{children:Ht[e.details.status||``]||e.details.status||`未记录结果`})}),e.details.subject_id?(0,x.jsxs)(`a`,{href:Wt(e.details.subject_id,e.details.version),children:[`查看人物 `,e.details.subject_id,e.details.version?` 的摘要 v${e.details.version}`:` 的摘要与任务`]}):(0,x.jsx)(`p`,{children:`人物摘要步骤异常,未关联具体人物。`}),e.reason&&(0,x.jsx)(`p`,{children:Ut(e.reason)}),e.details.attempt_id!=null&&(0,x.jsxs)(`p`,{children:[`生成任务 #`,e.details.attempt_id]}),(0,x.jsx)(`small`,{children:(0,x.jsx)(N,{value:e.created_at})})]},e.item_key)),(0,x.jsx)(T,{total:r.length,offset:t,change:n})]})}function on({data:e}){let t=e.items.filter(e=>e.outcome===`model_step_budget`),n=e.items.filter(e=>e.outcome===`backlog_trimmed`),r=e.question_expiry;return(0,x.jsxs)(x.Fragment,{children:[(e.consolidation||t.length>0)&&(0,x.jsxs)(`section`,{className:`detail-section`,"aria-label":`各步骤模型调用上限与用量`,children:[(0,x.jsx)(`h3`,{children:`各步骤模型调用上限与用量`}),(0,x.jsx)(`p`,{className:`fine-print`,children:`每一步独立计数,包含重试和修正;上限沿用本次运行接受时的设置。未到期、关闭或没有任务的步骤可能没有记录。`}),!t.length&&(0,x.jsx)(`p`,{className:`quiet`,children:`本次未记录步骤用量。`}),t.map(e=>(0,x.jsxs)(`article`,{className:`maintenance-item`,"aria-label":V[e.details.step||``]||e.details.step||`未知步骤`,children:[(0,x.jsx)(`h4`,{children:V[e.details.step||``]||e.details.step||`未知步骤`}),(0,x.jsxs)(`p`,{children:[`本轮上限:`,e.details.max_calls??`未记录`,` 次 · 已用:`,e.details.calls_used??`未记录`,` 次`]}),(0,x.jsxs)(`p`,{children:[`停止原因:`,e.reason?pe(e.reason):`未记录`]})]},e.item_key))]}),!!n.length&&(0,x.jsxs)(`section`,{className:`detail-section`,"aria-label":`配对积压修剪`,children:[(0,x.jsx)(`h3`,{children:`配对积压修剪`}),n.map(e=>(0,x.jsxs)(`p`,{children:[`配对积压修剪:`,e.details.count??`未记录`,` 项 · 待处理配对上限:`,e.details.limit??`未记录`,` 项`]},e.item_key)),(0,x.jsx)(`p`,{className:`fine-print`,children:`仅修剪待处理配对,不改变记忆;相关记忆发生变化后可重新规划。`})]}),r&&(0,x.jsxs)(`section`,{className:`detail-section`,"aria-label":`询问到期结束`,children:[(0,x.jsx)(`h3`,{children:`询问到期结束`}),(0,x.jsxs)(`p`,{children:[`到期自动结束的询问:`,r.count,` 条`]}),(0,x.jsx)(`p`,{className:`fine-print`,children:`仅学习产生且超过保留天数仍未结束的询问;宿主或管理员创建的询问和普通目标不受影响。`})]})]})}function sn({data:e,openMemory:t,refresh:n}){let r=e.summary.model_calls,i=e.persona;return(0,x.jsxs)(x.Fragment,{children:[e.consolidation&&(0,x.jsxs)(`section`,{className:`detail-section`,"aria-label":`本次整理与模型用量`,children:[(0,x.jsx)(`h3`,{children:`本次整理与模型用量`}),(0,x.jsxs)(`p`,{children:[`后续待处理项目:`,e.consolidation.deferred,` 项`,e.consolidation.skip_reason&&` · ${pe(e.consolidation.skip_reason)}`]}),r&&(0,x.jsxs)(x.Fragment,{children:[(0,x.jsxs)(`p`,{children:[`模型调用 `,r.count,` 次 · 输入 `,r.prompt_tokens??0,` `,`token · 输出 `,r.completion_tokens??0,` token · 总耗时`,` `,C(r.duration_ms??0)]}),(0,x.jsxs)(`p`,{className:`fine-print`,children:[r.unknown_usage_calls??0,` `,`次调用用量未知;用量为四步骤已知合计,包含实际重试、入口画像、persona 及人物摘要调用。`]})]}),(0,x.jsxs)(`details`,{children:[(0,x.jsx)(`summary`,{children:`本次接受任务时的整理设置`}),ue.map(([t,n])=>(0,x.jsxs)(`p`,{children:[n,`:`,e.consolidation?.settings[t]??`未记录`]},t)),(0,x.jsxs)(`p`,{children:[`处理方式:`,e.consolidation?.settings.resolution===`auto_v1`?`自主处理`:`只报告`]}),fe.map(([t,n])=>(0,x.jsxs)(`p`,{children:[n,`:`,e.consolidation?.settings[t]==null?`未记录`:e.consolidation.settings[t]?`开启`:`关闭`]},t)),(0,x.jsx)(`p`,{className:`fine-print`,children:`设置修改只影响之后接受的新运行。`})]})]}),(0,x.jsx)(on,{data:e}),(e.consolidation||e.items.some(e=>e.phase===`consolidation`))&&(0,x.jsxs)(x.Fragment,{children:[(0,x.jsx)(nn,{name:`记忆合并`,items:e.items.filter(e=>e.outcome===`merged`),openMemory:t,runId:e.id,refresh:n}),(0,x.jsx)(nn,{name:`自主处理结果`,items:e.items.filter(e=>e.details.auto_resolution===`auto_v1`&&e.outcome!==`merged`),openMemory:t,runId:e.id,refresh:n}),(0,x.jsx)(nn,{name:`整理建议(模型建议)`,items:e.items.filter(e=>[`conflicts`,`dependencies_reviewed`].includes(e.outcome)),openMemory:t,runId:e.id})]}),i&&(0,x.jsxs)(`section`,{className:`detail-section`,"aria-label":`persona 变化`,children:[(0,x.jsx)(`h3`,{children:`persona 变化`}),(0,x.jsxs)(`p`,{children:[(0,x.jsx)(M,{tone:i.status===`published`?`green`:``,children:{published:`已发布`,pending:`待确认,当前版本保留`,rejected:`已拒绝,当前版本保留`,not_due:`尚未到更新条件`,skipped:`已跳过`,failed:`失败`,conflict:`版本或依据冲突`,running:`更新中`}[i.status]||i.status}),i.change_degree&&` · 变化程度:${R[i.change_degree]||i.change_degree}`]}),i.reason&&(0,x.jsx)(`p`,{children:pe(i.reason)}),i.version_id&&i.base_version_id?(0,x.jsxs)(`a`,{href:`#/persona?tab=history&version=${i.version_id}&before=${i.base_version_id}&after=${i.version_id}`,children:[`对比 persona v`,i.base_version_id,` → v`,i.version_id]}):(0,x.jsx)(`a`,{href:`#/persona?tab=attempts`,children:`查看 persona 生成记录`}),i.content&&(0,x.jsxs)(`details`,{children:[(0,x.jsx)(`summary`,{children:`查看候选正文与检查`}),(0,x.jsx)(`p`,{className:`persona-content`,children:i.content}),i.checks&&(0,x.jsx)(Yt,{version:{checks:i.checks}})]})]}),(0,x.jsx)(an,{data:e}),(0,x.jsx)(rn,{data:e})]})}function cn({detail:e,busy:t,blocked:n,setBusy:r,saved:i,failed:a,openMemory:o,close:s}){let[c,u]=(0,l.useState)(null),d=t||n;async function f(){if(c&&!d){r(!0);try{i(await g(`/memories/${e.id}/annotations/${c.annotation.id}${c.kind===`confirm`?`/confirm`:``}`,_(c.kind===`confirm`?`POST`:`DELETE`,{expected_revision:c.revision}))),u(null)}catch(e){a(e)}finally{r(!1)}}}return(0,x.jsxs)(`section`,{className:`detail-section`,"aria-label":`整理建议`,children:[(0,x.jsxs)(`h3`,{children:[`整理建议 `,(0,x.jsx)(M,{children:`模型建议 · 仅管理员可见`})]}),(0,x.jsx)(`p`,{className:`fine-print`,children:`建议可能有误,请结合来源核对。未确认和已确认的建议都不会进入宿主召回。`}),!e.consolidation_annotations?.length&&(0,x.jsx)(`p`,{className:`quiet`,children:`暂无整理建议。`}),e.consolidation_annotations?.map(n=>(0,x.jsxs)(`article`,{className:`maintenance-item`,children:[(0,x.jsxs)(`div`,{className:`detail-badges`,children:[(0,x.jsx)(M,{tone:`warning`,children:n.report.suggestion_type===`merge_details`?Bt:ce[n.kind]||n.kind}),(0,x.jsx)(M,{children:le[n.review_status]}),n.modified_since_annotation&&(0,x.jsx)(M,{tone:`warning`,children:`标注后已修改`}),(0,x.jsxs)(`span`,{className:`muted`,children:[`建议 #`,n.id]})]}),n.report.suggestion_type===`merge_details`&&(0,x.jsx)(W,{details:n.report.unique_details,ids:n.report.detail_memory_ids||n.related_memory_ids,openMemory:o,disabled:t}),(0,x.jsx)(`p`,{className:`report-text`,children:n.text}),(0,x.jsxs)(`p`,{className:`muted`,children:[`标注时修订 `,n.memory_revision,` · `,(0,x.jsx)(N,{value:n.created_at})]}),n.confirmed_at&&(0,x.jsxs)(`p`,{children:[`管理员已确认 · `,(0,x.jsx)(N,{value:n.confirmed_at})]}),(0,x.jsxs)(`div`,{className:`actions`,children:[n.related_memory_ids.filter(t=>t!==e.id).map(e=>(0,x.jsxs)(`button`,{className:`text-button`,disabled:t,onClick:()=>o(e),children:[`相关记忆 #`,e]},e)),n.superseded_by!=null&&(0,x.jsxs)(`button`,{className:`text-button`,disabled:t,onClick:()=>o(n.superseded_by),children:[`建议的替代记忆 #`,n.superseded_by]})]}),n.report.reason&&(0,x.jsx)(`p`,{className:`report-text`,children:n.report.reason}),(0,x.jsx)($t,{value:n.report.source_excerpts,openMemory:o}),(0,x.jsxs)(`a`,{href:`#/status?run=${n.report.run_id}`,onClick:e=>{t?e.preventDefault():s()},children:[`查看整理报告 #`,n.report.run_id]}),(0,x.jsxs)(`div`,{className:`actions suggestion-actions`,children:[n.review_status===`pending`&&(0,x.jsxs)(`button`,{disabled:d||!!c,onClick:()=>u({kind:`confirm`,annotation:n,revision:e.revision}),children:[`采纳建议 #`,n.id]}),(0,x.jsxs)(`button`,{disabled:d||!!c,onClick:()=>u({kind:`clear`,annotation:n,revision:e.revision}),children:[`清除建议 #`,n.id]})]}),c?.annotation.id===n.id&&(0,x.jsxs)(`div`,{className:`delete-confirm`,role:`group`,"aria-label":`确认建议操作`,children:[(0,x.jsx)(`p`,{children:c.kind===`confirm`?n.report.suggestion_type===`merge_details`?`采纳只记录管理员确认,不执行合并,不改正文和相信程度;建议仍只对管理员可见。`:`采纳只记录管理员确认,不改正文和相信程度;建议仍只对管理员可见。`:`清除会撤下这条整理建议并保留操作记录,不改正文和相信程度。`}),(0,x.jsxs)(`div`,{className:`actions`,children:[(0,x.jsx)(`button`,{className:`primary`,disabled:d,onClick:()=>void f(),children:c.kind===`confirm`?`确认采纳建议`:`确认清除建议`}),(0,x.jsx)(`button`,{disabled:t,onClick:()=>u(null),children:`取消建议操作`})]})]})]},n.id))]})}function ln({notice:e}){return e?(0,x.jsx)(A,{children:e.message}):null}var un={memory_source:`仍是其他记忆的来源`,batch_segment:`等待或运行中的批次仍在使用`,goal_source:`仍是目标的来源`,subject_alias:`仍是主体别名的证据`,subject_link:`仍是主体联系的证据`,learning_request:`立即学习请求仍在使用`};function dn({result:e}){return(0,x.jsxs)(`div`,{className:`lifecycle-stack`,children:[(0,x.jsxs)(A,{children:[`已彻底清除记忆 #`,e.memory_id]}),(0,x.jsx)(ln,{notice:e.backup_notice}),(0,x.jsxs)(`section`,{children:[(0,x.jsx)(`h3`,{children:`已删除的来源消息`}),e.deleted_message_ids.length?(0,x.jsx)(`ul`,{children:e.deleted_message_ids.map(e=>(0,x.jsxs)(`li`,{children:[`消息 #`,e,` · 没有其他受保护的引用`]},e))}):(0,x.jsx)(`p`,{className:`quiet`,children:`没有删除来源消息。`})]}),(0,x.jsxs)(`section`,{children:[(0,x.jsx)(`h3`,{children:`被保留的来源消息及原因`}),e.retained_messages.length?(0,x.jsx)(`ul`,{children:e.retained_messages.map(e=>(0,x.jsxs)(`li`,{children:[`消息 #`,e.message_id,` ·`,` `,e.reasons.map(e=>un[e]||e).join(`;`)]},e.message_id))}):(0,x.jsx)(`p`,{className:`quiet`,children:`没有被保留的来源消息。`})]})]})}function fn({detail:e,busy:t,blocked:n,setBusy:r,saved:i,failed:a,purged:o,recreated:s}){let[c,u]=(0,l.useState)(null),[d,f]=(0,l.useState)(!1),[p,m]=(0,l.useState)(``),[h,v]=(0,l.useState)(``),[y,b]=(0,l.useState)(``),[S,C]=(0,l.useState)(``),w=t||n,T=new Map;for(let t of e.revisions)T.set(t.revision_after,t.after);for(let t of e.revisions)T.set(t.revision_before,t.before);let E=T.get(Number(S));async function D(t,n={}){if(!w){r(!0);try{let r=`/memories/${e.id}/${t}`,a=_(t===`lifecycle`?`PATCH`:`POST`,{expected_revision:e.revision,...n});t===`purge`?o(await g(r,a)):t===`recreate`?s((await g(r,a)).id):i(await g(r,a)),u(null),f(!1)}catch(e){a(e)}finally{r(!1)}}}function O(t){if(t.preventDefault(),b(``),[p,h].some(e=>!e.trim()||!Number.isInteger(Number(e))||Number(e)<0||Number(e)>100)){b(`重要度和保留强度须为 0—100 的整数`);return}let n={};if(Number(p)!==e.importance&&(n.importance=Number(p)),Number(h)!==e.retention&&(n.retention=Number(h)),!Object.keys(n).length){u(null);return}D(`lifecycle`,n)}let k=()=>{u(null),f(!1),b(``)};return(0,x.jsxs)(`section`,{className:`detail-section lifecycle-stack`,"aria-label":`生命周期操作`,children:[(0,x.jsx)(`h3`,{children:`生命周期操作`}),!c&&(0,x.jsxs)(`div`,{className:`actions`,children:[e.lifecycle!==`deleted`&&(0,x.jsxs)(x.Fragment,{children:[(0,x.jsx)(`button`,{className:`secondary`,disabled:w,onClick:()=>void D(`lifecycle`,{pinned:!e.pinned}),children:e.pinned?`取消置顶`:`置顶记忆`}),e.lifecycle===`forgotten`?(0,x.jsx)(`button`,{className:`secondary`,disabled:w,onClick:()=>void D(`restore`),children:`恢复记忆`}):(0,x.jsx)(`button`,{className:`secondary`,disabled:w,onClick:()=>u(`forget`),children:`手动遗忘`}),(0,x.jsx)(`button`,{className:`secondary`,disabled:w,onClick:()=>{u(`scores`),m(String(e.importance)),v(String(e.retention))},children:`调整分数`})]}),(0,x.jsx)(`button`,{className:`secondary`,disabled:w||!T.size,onClick:()=>{C(``),u(`recreate`)},children:`按旧内容新建`}),(0,x.jsx)(`button`,{className:`text-button danger-text`,disabled:w,onClick:()=>u(`purge`),children:`彻底清除`})]}),!T.size&&(0,x.jsx)(`p`,{className:`fine-print`,children:`尚无历史修订可用于“按旧内容新建”。`}),!!e.pinned&&e.lifecycle!==`deleted`&&(0,x.jsx)(`p`,{className:`lifecycle-help`,children:`置顶记忆不自动衰减、遗忘、扣减或删除。置顶不会自动恢复已遗忘的记忆。`}),c===`scores`&&(0,x.jsx)(`form`,{onSubmit:O,noValidate:!0,children:(0,x.jsxs)(`fieldset`,{className:`lifecycle-fieldset`,disabled:w,children:[(0,x.jsxs)(`div`,{className:`form-grid`,children:[(0,x.jsxs)(`label`,{children:[`调整重要度`,(0,x.jsx)(`input`,{type:`number`,min:0,max:100,step:1,required:!0,value:p,onChange:e=>m(e.target.value)})]}),(0,x.jsxs)(`label`,{children:[`调整保留强度`,(0,x.jsx)(`input`,{type:`number`,min:0,max:100,step:1,required:!0,value:h,onChange:e=>v(e.target.value)})]})]}),(0,x.jsx)(`p`,{className:`lifecycle-help`,children:`保留强度调整会按当前阈值判断遗忘或恢复。`}),y&&(0,x.jsx)(A,{error:!0,children:y}),(0,x.jsxs)(`div`,{className:`actions`,children:[(0,x.jsx)(`button`,{className:`primary`,children:`保存分数`}),(0,x.jsx)(`button`,{className:`secondary`,type:`button`,onClick:k,children:`取消调整`})]})]})}),c===`forget`&&(0,x.jsxs)(`div`,{className:`delete-confirm`,children:[(0,x.jsx)(`p`,{children:`确认手动遗忘?保留强度会降至遗忘阈值以下,并取消置顶;此后只参与深度召回。恢复时不会重新置顶。`}),(0,x.jsxs)(`div`,{className:`actions`,children:[(0,x.jsx)(`button`,{className:`primary`,disabled:w,onClick:()=>void D(`forget`),children:`确认遗忘`}),(0,x.jsx)(`button`,{className:`secondary`,disabled:t,onClick:k,children:`取消遗忘`})]})]}),c===`purge`&&(0,x.jsxs)(`div`,{className:`purge-confirm`,children:[(0,x.jsx)(`h3`,{children:`彻底清除不可撤销`}),(0,x.jsx)(`p`,{children:`将删除记忆正文、修订历史,以及只被这条记忆引用且没有其他受保护引用的来源消息。其他引用仍需要的消息会保留,执行后显示消息标识与原因。`}),(0,x.jsx)(`p`,{children:`同时清空可关联的模型原始输出等正文副本。`}),(0,x.jsx)(ln,{notice:e.purge_preview?.backup_notice}),(0,x.jsxs)(`label`,{className:`check-label`,children:[(0,x.jsx)(`input`,{type:`checkbox`,checked:d,disabled:w,onChange:e=>f(e.target.checked)}),`我理解此操作不可撤销`]}),(0,x.jsxs)(`div`,{className:`actions`,children:[(0,x.jsx)(`button`,{className:`danger-button`,disabled:w||!d,onClick:()=>void D(`purge`,{confirm:!0}),children:`再次确认并彻底清除`}),(0,x.jsx)(`button`,{className:`secondary`,disabled:t,onClick:k,children:`取消清除`})]})]}),c===`recreate`&&(0,x.jsxs)(`div`,{className:`lifecycle-stack`,children:[(0,x.jsxs)(`label`,{children:[`选择历史修订`,(0,x.jsxs)(`select`,{disabled:w,value:S,onChange:e=>C(e.target.value),children:[(0,x.jsx)(`option`,{value:``,children:`请选择修订`}),[...T.keys()].sort((e,t)=>t-e).map(e=>(0,x.jsxs)(`option`,{value:e,children:[`修订 `,e]},e))]})]}),E&&(0,x.jsxs)(`div`,{className:`revision`,"data-testid":`revision-preview`,children:[(0,x.jsx)(`p`,{children:String(E.content??``)}),(0,x.jsxs)(`small`,{children:[`相信程度 `,String(E.belief??e.belief),` · 重要度`,` `,String(E.importance??e.importance)]})]}),(0,x.jsx)(`p`,{className:`lifecycle-help`,children:`新建使用新标识,原记忆保持原状。正文和判断取所选修订,涉及人物、标签和来源沿用当前保存的关系;不继承置顶、遗忘时间或向量。`}),(0,x.jsxs)(`div`,{className:`actions`,children:[(0,x.jsx)(`button`,{className:`primary`,disabled:w||!E,onClick:()=>void D(`recreate`,{source_revision:Number(S)}),children:`确认新建`}),(0,x.jsx)(`button`,{className:`secondary`,disabled:t,onClick:k,children:`取消新建`})]})]})]})}function pn({version:e,openMemory:t,onChange:n}){let[r,i]=(0,l.useState)(0),a=y(`/memories/upcoming-deletion?limit=30&offset=${r}`),[o,s]=(0,l.useState)(null),[c,u]=(0,l.useState)(``),[d,p]=(0,l.useState)(``),[m,h]=(0,l.useState)(!1);(0,l.useEffect)(()=>{a.refresh()},[e,a.refresh]),(0,l.useEffect)(()=>{a.data&&r&&r>=a.data.total&&i(Math.max(0,Math.ceil(a.data.total/30)*30-30))},[a.data,r]);async function b(e){if(!(o!==null||m||a.loading||a.error)){s(e.id),u(``),p(``);try{await g(`/memories/${e.id}/restore`,_(`POST`,{expected_revision:e.revision})),p(`已恢复记忆 #${e.id}`),a.refresh(),n()}catch(e){e instanceof f&&(e.status===409||e.status===404)?(h(!0),u(`这条记忆已变化或已被删除,请刷新列表后再操作。`)):u(v(e))}finally{s(null)}}}return(0,x.jsxs)(x.Fragment,{children:[(0,x.jsxs)(`div`,{className:`panel-heading`,children:[(0,x.jsx)(`h2`,{children:`即将删除`}),(0,x.jsx)(`button`,{className:`secondary`,disabled:o!==null,onClick:()=>{h(!1),u(``),a.refresh()},children:`刷新列表`})]}),(0,x.jsx)(`p`,{className:`learning-intro`,children:`包括遗忘期将满和已到期等待维护的记忆;置顶记忆不自动删除。到期后由维护执行删除,可以在此提前恢复。`}),(c||a.error)&&(0,x.jsx)(A,{error:!0,children:c||a.error}),d&&(0,x.jsx)(A,{children:d}),!a.data&&!a.error&&(0,x.jsx)(`p`,{role:`status`,children:`正在读取即将删除的记忆…`}),a.data&&!a.data.enabled&&(0,x.jsxs)(A,{children:[`自动删除已关闭,当前没有即将删除的记忆。`,(0,x.jsx)(`a`,{href:`#/settings`,children:`前往生命周期设置`})]}),a.data?.enabled&&!a.data.items.length&&(0,x.jsx)(j,{title:`暂无即将删除的记忆`}),(0,x.jsx)(`div`,{className:`memory-grid`,children:a.data?.items.map(e=>(0,x.jsxs)(`article`,{className:`panel upcoming-card`,children:[(0,x.jsxs)(`div`,{className:`memory-card-top`,children:[(0,x.jsx)(M,{children:`已遗忘`}),(0,x.jsxs)(`span`,{className:`muted`,children:[`#`,e.id,` · 保留强度 `,e.retention]})]}),(0,x.jsx)(`button`,{className:`memory-preview`,onClick:()=>t(e.id),children:e.content}),(0,x.jsxs)(`p`,{children:[`遗忘时间:`,w(e.forgotten_at)]}),(0,x.jsxs)(`p`,{children:[`删除到期时间:`,w(e.delete_after)]}),(0,x.jsx)(`button`,{className:`secondary`,"aria-label":`恢复记忆 #${e.id}`,disabled:o!==null||m||a.loading||!!a.error,onClick:()=>void b(e),children:o===e.id?`正在恢复…`:`恢复记忆`})]},e.id))}),a.data&&(0,x.jsx)(T,{total:a.data.total,offset:r,change:i})]})}var mn=[`事件`,`事实`,`偏好`,`关系`,`观点`,`计划`,`自我`,`其他`];function hn({openMemory:e,version:t,onChange:n,initialQuery:r}){let[i,a]=(0,l.useState)(`all`);return(0,x.jsxs)(x.Fragment,{children:[(0,x.jsx)(`div`,{className:`page-heading`,children:(0,x.jsxs)(`div`,{children:[(0,x.jsx)(`p`,{className:`eyebrow`,children:`留下的经历,有迹可循`}),(0,x.jsx)(`h1`,{children:`记忆`}),(0,x.jsx)(`p`,{children:`查看内容、追溯来源,管理记忆的保留与遗忘。`})]})}),(0,x.jsxs)(`div`,{className:`learning-tabs`,"aria-label":`记忆视图`,children:[(0,x.jsx)(`button`,{"aria-pressed":i===`all`,onClick:()=>a(`all`),children:`记忆列表`}),(0,x.jsx)(`button`,{"aria-pressed":i===`upcoming`,onClick:()=>a(`upcoming`),children:`即将删除`})]}),i===`all`?(0,x.jsx)(gn,{openMemory:e,version:t,initialQuery:r}):(0,x.jsx)(pn,{openMemory:e,version:t,onChange:n})]})}function gn({openMemory:e,version:t,initialQuery:n=``}){let r=y(`/catalog`),i=new URLSearchParams(n),[a,o]=(0,l.useState)({text:``,person_id:i.get(`person_id`)||``,kind:``,entry_id:``,time_from:``,time_to:``,lifecycle:i.get(`lifecycle`)===`all`?`all`:`active`,pinned:``,sort:`time`}),[s,c]=(0,l.useState)(()=>new URLSearchParams(Object.entries(a).filter(([,e])=>!!e)).toString()),[u,d]=(0,l.useState)(0),[f,p]=(0,l.useState)(``),m=y(`/memories?${s}&limit=30&offset=${u}`);(0,l.useEffect)(()=>{m.refresh()},[t,m.refresh]),(0,l.useEffect)(()=>{m.data&&u&&u>=m.data.total&&d(Math.max(0,Math.ceil(m.data.total/30)*30-30))},[m.data,u]);function h(e){if(e.preventDefault(),a.time_from&&a.time_to&&a.time_from>a.time_to){p(`开始日期不能晚于结束日期`);return}p(``),d(0),c(new URLSearchParams(Object.entries(a).filter(([,e])=>!!e)).toString()),m.refresh()}let g=(e,t)=>o({...a,[e]:t});return(0,x.jsxs)(x.Fragment,{children:[(0,x.jsx)(`section`,{className:`panel`,children:(0,x.jsxs)(`form`,{className:`memory-filters`,onSubmit:h,children:[(0,x.jsxs)(`div`,{className:`search-row`,children:[(0,x.jsxs)(`label`,{className:`search-field`,children:[`搜索记忆`,(0,x.jsx)(`input`,{value:a.text,maxLength:1e3,placeholder:`输入正文或标签中的词…`,onChange:e=>g(`text`,e.target.value)})]}),(0,x.jsx)(`button`,{className:`primary`,type:`submit`,children:`搜索`})]}),(0,x.jsxs)(`div`,{className:`filter-grid`,children:[(0,x.jsxs)(`label`,{children:[`涉及的人`,(0,x.jsxs)(`select`,{value:a.person_id,onChange:e=>g(`person_id`,e.target.value),children:[(0,x.jsx)(`option`,{value:``,children:`全部人物`}),r.data?.people.map(e=>(0,x.jsxs)(`option`,{value:e.id,children:[e.name,(r.data?.people.filter(t=>t.name===e.name).length||0)>1?` · ${e.id.slice(0,6)}`:``]},e.id))]})]}),(0,x.jsxs)(`label`,{children:[`记忆类型`,(0,x.jsxs)(`select`,{value:a.kind,onChange:e=>g(`kind`,e.target.value),children:[(0,x.jsx)(`option`,{value:``,children:`全部类型`}),mn.map(e=>(0,x.jsx)(`option`,{children:e},e))]})]}),(0,x.jsxs)(`label`,{children:[`出处入口`,(0,x.jsxs)(`select`,{value:a.entry_id,onChange:e=>g(`entry_id`,e.target.value),children:[(0,x.jsx)(`option`,{value:``,children:`全部入口`}),r.data?.entries.map(e=>(0,x.jsx)(`option`,{value:e.id,children:e.name},e.id))]})]}),(0,x.jsxs)(`label`,{children:[`状态`,(0,x.jsxs)(`select`,{value:a.lifecycle,onChange:e=>g(`lifecycle`,e.target.value),children:[(0,x.jsx)(`option`,{value:`active`,children:`有效`}),(0,x.jsx)(`option`,{value:`forgotten`,children:`已遗忘`}),(0,x.jsx)(`option`,{value:`deleted`,children:`已删除(历史)`}),(0,x.jsx)(`option`,{value:`all`,children:`全部状态`})]})]}),(0,x.jsxs)(`label`,{children:[`置顶筛选`,(0,x.jsxs)(`select`,{"aria-label":`置顶筛选`,value:a.pinned,onChange:e=>g(`pinned`,e.target.value),children:[(0,x.jsx)(`option`,{value:``,children:`全部`}),(0,x.jsx)(`option`,{value:`true`,children:`已置顶`}),(0,x.jsx)(`option`,{value:`false`,children:`未置顶`})]})]}),(0,x.jsxs)(`label`,{children:[`开始日期`,(0,x.jsx)(`input`,{type:`date`,value:a.time_from,onChange:e=>g(`time_from`,e.target.value)})]}),(0,x.jsxs)(`label`,{children:[`结束日期`,(0,x.jsx)(`input`,{type:`date`,value:a.time_to,onChange:e=>g(`time_to`,e.target.value)})]}),(0,x.jsxs)(`label`,{children:[`排序方式`,(0,x.jsxs)(`select`,{value:a.sort,onChange:e=>g(`sort`,e.target.value),children:[(0,x.jsx)(`option`,{value:`time`,children:`更新时间 · 从新到旧`}),(0,x.jsx)(`option`,{value:`relevance`,children:`文本相关度`}),(0,x.jsx)(`option`,{value:`retention`,children:`保留强度 · 从高到低`})]})]})]}),(0,x.jsx)(`p`,{className:`fine-print`,children:`日期按事件时间筛选;没有事件时间时使用创建时间。`})]})}),f&&(0,x.jsx)(A,{error:!0,children:f}),(m.error||r.error)&&(0,x.jsx)(A,{error:!0,children:m.error||r.error}),(0,x.jsxs)(`div`,{className:`list-heading`,children:[(0,x.jsx)(`span`,{children:m.data?`共 ${m.data.total} 条`:`正在读取…`}),(0,x.jsx)(`span`,{children:`浏览列表不会增加召回或使用次数`})]}),m.data?.items.length===0&&(0,x.jsx)(`section`,{className:`panel`,children:(0,x.jsx)(j,{title:`没有找到记忆`,children:`尝试换一个词,或调整筛选条件。`})}),(0,x.jsx)(`div`,{className:`memory-grid`,children:m.data?.items.map(t=>(0,x.jsxs)(`button`,{className:`memory-card panel`,onClick:()=>e(t.id),children:[(0,x.jsxs)(`div`,{className:`memory-card-top`,children:[(0,x.jsx)(M,{tone:`purple`,children:t.kind}),(0,x.jsx)(M,{tone:t.pinned?`purple`:``,children:t.pinned?`已置顶`:`未置顶`}),(0,x.jsxs)(`span`,{className:`muted`,children:[`#`,t.id,` · `,E(t.lifecycle)]})]}),(0,x.jsx)(`p`,{className:`memory-content`,children:t.content}),(0,x.jsx)(`div`,{className:`person-line`,children:t.about.map(e=>e.name).join(`、`)||`未标注涉及人物`}),(0,x.jsxs)(`div`,{className:`memory-card-bottom`,children:[(0,x.jsxs)(`span`,{children:[`相信 `,t.belief,` · 保留强度 `,t.retention]}),(0,x.jsx)(`time`,{children:S(t.updated_at)}),(0,x.jsx)(`span`,{"aria-hidden":!0,children:`↗`})]})]},t.id))}),m.data&&(0,x.jsx)(T,{total:m.data.total,offset:u,change:d})]})}function _n({id:e,onClose:t,onChange:n,openMemory:r}){let i=y(`/memories/${e}`),a=y(`/catalog`),[o,s]=(0,l.useState)(null),[c,u]=(0,l.useState)(!1),[d,p]=(0,l.useState)(``),[m,h]=(0,l.useState)(!1),[b,C]=(0,l.useState)(``),[T,D]=(0,l.useState)(!1),[O,k]=(0,l.useState)(!1),[j,N]=(0,l.useState)(null),[P,F]=(0,l.useState)(0),I=j?null:o||i.data,te=e=>{O||r(e)};function L(e){e instanceof f&&e.code===`revision_conflict`?(D(!0),C(`其他操作已更新了这条记忆。草稿暂时保留;请刷新并载入最新修订,再核对后操作。载入会替换当前草稿。`)):e instanceof f&&e.status===404?(D(!0),C(`这条记忆已删除或已彻底清除,请刷新或返回列表查看。`)):C(v(e))}async function ne(t){if(t.preventDefault(),!(!I||O||T)){k(!0),C(``);try{s(await g(`/memories/${e}`,_(`PATCH`,{expected_revision:I.revision,content:d}))),u(!1),D(!1),n()}catch(e){L(e)}finally{k(!1)}}}async function R(){if(!(!I||O||T)){k(!0),C(``);try{s(await g(`/memories/${e}`,_(`DELETE`,{expected_revision:I.revision}))),h(!1),F(e=>e+1),u(!1),n()}catch(e){L(e)}finally{k(!1)}}}async function re(){k(!0);try{let t=await g(`/memories/${e}`);s(t),p(t.content),F(e=>e+1),D(!1),C(``),h(!1)}catch(e){L(e)}finally{k(!1)}}return(0,x.jsxs)(ee,{title:`记忆 #${e}`,onClose:t,closeDisabled:O,children:[(b||i.error)&&(0,x.jsx)(A,{error:!0,children:b||i.error}),T&&(0,x.jsx)(`button`,{className:`secondary`,disabled:O,onClick:re,children:`载入最新修订`}),j&&(0,x.jsx)(dn,{result:j}),!I&&!j&&!i.error&&(0,x.jsx)(`p`,{className:`quiet`,children:`正在读取详情…`}),I&&(0,x.jsxs)(`div`,{className:`memory-detail`,children:[(0,x.jsxs)(`div`,{className:`detail-badges`,children:[(0,x.jsx)(M,{tone:`purple`,children:I.kind}),(0,x.jsx)(M,{children:I.stance}),(0,x.jsx)(M,{tone:I.lifecycle===`deleted`?`danger`:``,children:E(I.lifecycle)}),(0,x.jsx)(M,{tone:I.pinned?`purple`:``,children:I.pinned?`已置顶`:`未置顶`}),(0,x.jsxs)(`span`,{className:`muted`,children:[`修订 `,I.revision]})]}),I.merged_into!=null&&(0,x.jsxs)(A,{children:[(0,x.jsxs)(`button`,{className:`text-button`,disabled:O,onClick:()=>te(I.merged_into),children:[`已合并到 #`,I.merged_into]}),(0,x.jsx)(`p`,{children:`全部来源已归入保留记忆;此处是原记忆占位,不能恢复为有效记忆。`})]}),I.lifecycle===`deleted`&&I.merged_into==null&&(0,x.jsx)(A,{children:`此标识已撤销,不再参与召回;来源和修订历史仍然保留。`}),c&&I.lifecycle!==`deleted`?(0,x.jsxs)(`form`,{onSubmit:ne,children:[(0,x.jsxs)(`label`,{children:[`记忆正文`,(0,x.jsx)(`textarea`,{value:d,maxLength:1e3,rows:5,onChange:e=>p(e.target.value)})]}),(0,x.jsxs)(`div`,{className:`actions`,children:[(0,x.jsx)(`button`,{className:`primary`,disabled:O||T||!d.trim(),children:`保存修改`}),(0,x.jsx)(`button`,{type:`button`,className:`secondary`,disabled:O,onClick:()=>u(!1),children:`取消编辑`}),(0,x.jsxs)(`small`,{children:[d.length,` / 1000 字`]})]})]}):(0,x.jsx)(`p`,{className:`detail-content`,children:I.content}),(0,x.jsxs)(`div`,{className:`score-grid`,children:[(0,x.jsxs)(`div`,{children:[(0,x.jsx)(`strong`,{children:I.belief}),(0,x.jsx)(`span`,{children:`相信程度`})]}),(0,x.jsxs)(`div`,{children:[(0,x.jsx)(`strong`,{children:I.importance}),(0,x.jsx)(`span`,{children:`重要度`})]}),(0,x.jsxs)(`div`,{children:[(0,x.jsx)(`strong`,{children:I.retention}),(0,x.jsx)(`span`,{children:`保留强度`})]})]}),(0,x.jsx)(fn,{detail:I,busy:O,blocked:T||c||m,setBusy:k,failed:L,saved:e=>{s(e),C(``),n()},purged:e=>{N(e),s(null),C(``),n()},recreated:e=>{n(),r(e)}},`${I.id}:${I.revision}:${P}`),(0,x.jsxs)(`dl`,{className:`metadata`,children:[(0,x.jsxs)(`div`,{children:[(0,x.jsx)(`dt`,{children:`记忆可见范围`}),(0,x.jsx)(`dd`,{children:nt(I.visibility,a.data?.entries||[])})]}),I.lifecycle===`forgotten`&&(0,x.jsxs)(`div`,{children:[(0,x.jsx)(`dt`,{children:`遗忘时间`}),(0,x.jsx)(`dd`,{children:w(I.forgotten_at)})]}),(0,x.jsxs)(`div`,{children:[(0,x.jsx)(`dt`,{children:`说话人`}),(0,x.jsx)(`dd`,{children:I.speaker.name})]}),(0,x.jsxs)(`div`,{children:[(0,x.jsx)(`dt`,{children:`涉及的人`}),(0,x.jsx)(`dd`,{children:I.about.map(e=>e.name).join(`、`)||`未标注`})]}),(0,x.jsxs)(`div`,{children:[(0,x.jsx)(`dt`,{children:`事件时间`}),(0,x.jsx)(`dd`,{children:I.event_time||`未标注`})]}),(0,x.jsxs)(`div`,{children:[(0,x.jsx)(`dt`,{children:`更新时间`}),(0,x.jsx)(`dd`,{children:S(I.updated_at)})]}),(0,x.jsxs)(`div`,{children:[(0,x.jsx)(`dt`,{children:`被召回`}),(0,x.jsxs)(`dd`,{children:[I.recall_count,` 次`]})]}),(0,x.jsxs)(`div`,{children:[(0,x.jsx)(`dt`,{children:`被使用`}),(0,x.jsxs)(`dd`,{children:[I.used_count,` 次`]})]})]}),(0,x.jsx)(`p`,{className:`fine-print`,children:`被召回不等于被使用;使用次数不代表内容可信度。`}),!!I.tags.length&&(0,x.jsx)(`div`,{className:`tag-list`,children:I.tags.map(e=>(0,x.jsxs)(M,{children:[`# `,e]},e))}),(0,x.jsx)(cn,{detail:I,busy:O,blocked:T||c||m,setBusy:k,saved:e=>{s(e),C(``),n()},failed:L,openMemory:te,close:t},`suggestions-${I.id}:${I.revision}:${P}`),(0,x.jsxs)(`section`,{className:`detail-section`,children:[(0,x.jsx)(`h3`,{children:`来源与前后文`}),I.sources.length?I.sources.map(e=>(0,x.jsx)(vn,{source:e,openMemory:te},e.id)):(0,x.jsx)(`p`,{className:`quiet`,children:`没有保存的来源消息。`})]}),(0,x.jsxs)(`section`,{className:`detail-section`,children:[(0,x.jsx)(`h3`,{children:`基于它形成的记忆`}),I.derived_memories.length?I.derived_memories.map(e=>(0,x.jsxs)(`button`,{className:`memory-preview`,disabled:O,onClick:()=>te(e.id),children:[(0,x.jsxs)(`span`,{children:[`#`,e.id,` · `,e.content]}),(0,x.jsxs)(`small`,{children:[E(e.lifecycle),e.needs_review?` · 来源已变化,待复核`:``]})]},e.id)):(0,x.jsx)(`p`,{className:`quiet`,children:`暂无派生记忆。`})]}),(0,x.jsxs)(`details`,{className:`history`,children:[(0,x.jsxs)(`summary`,{children:[`修订历史 `,(0,x.jsx)(M,{children:I.revisions.length})]}),I.revisions.length?I.revisions.map(e=>(0,x.jsx)(yn,{revision:e},e.id)):(0,x.jsx)(`p`,{className:`quiet`,children:`还没有修订记录。`})]}),(0,x.jsx)(`a`,{className:`operation-link`,href:`#/operations?object_type=memory&object_id=${e}`,onClick:e=>{O?e.preventDefault():t()},children:`查看此记忆的操作记录`}),I.lifecycle!==`deleted`&&(0,x.jsx)(`div`,{className:`detail-footer`,children:m?(0,x.jsxs)(`div`,{className:`delete-confirm`,children:[(0,x.jsx)(`p`,{children:`确认删除这条记忆?此标识将永久撤销,共享的来源消息与其他记忆保留。`}),(0,x.jsxs)(`div`,{className:`actions`,children:[(0,x.jsx)(`button`,{className:`danger-button`,disabled:O||T,onClick:R,children:`确认删除`}),(0,x.jsx)(`button`,{className:`secondary`,disabled:O,onClick:()=>h(!1),children:`取消`})]})]}):(0,x.jsxs)(`div`,{className:`actions`,children:[(0,x.jsx)(`button`,{className:`secondary`,disabled:O||T||c,onClick:()=>{u(!0),p(I.content),C(``)},children:`编辑正文`}),(0,x.jsx)(`button`,{className:`text-button danger-text`,disabled:O||T||c,onClick:()=>h(!0),children:`删除记忆`})]})})]})]})}function vn({source:e,openMemory:t,initiallyOpen:n=!1,speakerNames:r={}}){return(0,x.jsxs)(`details`,{className:`source`,open:n||void 0,children:[(0,x.jsx)(`summary`,{children:e.kind===`message`?`来源消息 · ${e.message?.sender_name||`原文已不可用`}`:e.kind===`memory`?`来源记忆 #${e.memory?.id||``}`:`初始背景`}),e.message&&(0,x.jsxs)(x.Fragment,{children:[(0,x.jsxs)(`p`,{className:`muted`,children:[e.message.entry_name||e.message.entry_id,` ·`,` `,S(e.message.occurred_at)]}),(0,x.jsx)(`blockquote`,{children:e.message.content}),e.message.quote_content&&(0,x.jsxs)(`blockquote`,{children:[`转引原话(原作者:`,r[e.message.quote_author_subject_id||``]||e.message.quote_author_subject_id||`未记录`,`):`,e.message.quote_content]}),(0,x.jsx)(qe,{media:e.message.media}),(0,x.jsxs)(`details`,{className:`source-context`,children:[(0,x.jsx)(`summary`,{children:`查看前后文`}),e.context?.map(t=>(0,x.jsxs)(`div`,{className:`context-message ${t.id===e.message_id?`highlight`:``}`,children:[(0,x.jsxs)(`small`,{children:[t.sender_name,` · `,S(t.occurred_at)]}),(0,x.jsx)(`p`,{children:t.content}),(0,x.jsx)(qe,{media:t.media})]},t.id))]})]}),e.memory&&(0,x.jsxs)(x.Fragment,{children:[(0,x.jsx)(`button`,{className:`memory-preview`,onClick:()=>t(e.memory.id),children:e.memory.content}),(0,x.jsxs)(`p`,{className:`fine-print`,children:[`依据修订 `,e.source_revision,` · `,E(e.memory.lifecycle),e.needs_review?` · 来源已变化,待复核`:``]})]}),e.kind===`initial_setting`&&(0,x.jsx)(`p`,{children:e.note||`角色初始背景材料`})]})}function yn({revision:e}){return(0,x.jsxs)(`article`,{className:`revision`,children:[(0,x.jsxs)(`small`,{children:[S(e.created_at),` ·`,` `,{admin:`管理员`,learning:`学习`,maintenance:`维护`,entry_profile:`画像学习`}[e.actor]||e.actor,` `,`· 修订 `,e.revision_before,` → `,e.revision_after]}),(0,x.jsxs)(`p`,{children:[(0,x.jsx)(`span`,{className:`muted`,children:`修改前`}),` `,String(e.before.content||``)]}),(0,x.jsxs)(`p`,{className:`muted`,children:[`相信程度 `,String(e.before.belief??`—`),` →`,` `,String(e.after.belief??`—`),` · 重要度`,` `,String(e.before.importance??`—`),` →`,` `,String(e.after.importance??`—`)]}),(0,x.jsxs)(`p`,{children:[(0,x.jsx)(`span`,{className:`muted`,children:`修改后`}),` `,e.after.lifecycle===`deleted`?`已删除(撤销对象)`:String(e.after.content||``)]})]})}function bn({goal:e,openMemory:t}){let[n,r]=(0,l.useState)(0),i=y(`/goals/${e.id}/sources?limit=30&offset=${n}`);return(0,x.jsxs)(`section`,{className:`panel goals-panel`,"aria-label":`来源消息`,children:[(0,x.jsxs)(`div`,{className:`panel-heading`,children:[(0,x.jsx)(`h2`,{children:`来源消息`}),(0,x.jsx)(`button`,{className:`text-button`,onClick:i.refresh,children:`刷新来源消息`})]}),(0,x.jsx)(`p`,{className:`goal-help`,children:`前后文来自同一入口的相邻消息,只读查看不会成为新经历。`}),i.error&&(0,x.jsxs)(A,{error:!0,children:[i.error,(0,x.jsx)(`button`,{onClick:i.refresh,children:`重试来源消息`})]}),!i.data&&!i.error&&(0,x.jsx)(`p`,{role:`status`,children:`正在读取来源消息…`}),i.data?.total===0&&(0,x.jsx)(`p`,{className:`goal-help`,children:`没有关联来源消息。宿主和管理员创建的目标可以没有对话依据。`}),i.data?.items.map(e=>(0,x.jsx)(`div`,{children:e.missing||!e.message?(0,x.jsxs)(`div`,{className:`source`,children:[(0,x.jsxs)(`p`,{children:[`来源消息 #`,e.message_id]}),(0,x.jsx)(`p`,{className:`goal-help`,children:e.notice||`来源消息已清理或不可用`})]}):(0,x.jsx)(vn,{source:{id:e.id,kind:`message`,message_id:e.message_id,message:e.message,context:e.context},openMemory:t})},e.id)),i.data&&(0,x.jsx)(T,{total:i.data.total,offset:n,change:r})]})}var xn={content:`正文`,deadline:`截止时间`,deadline_at:`截止时刻`,reminder_minutes:`提醒提前量`,state:`状态`,kind:`类型`,origin:`来源`,entry_id:`产生入口`,people:`涉及的人`,closed_at:`结束时间`,closed_by:`结束操作者`,merged_into:`合并到目标`,source_message_ids:`来源消息`,promise_memories:`承诺记忆及依据修订`,merged_goal_ids:`合并进来的目标`,duplicates:`可能重复关系`,basis_annotations:`依据复核标注`},Sn={create:`创建目标`,update:`更新目标`,merge:`合并目标`,duplicate_dismiss:`驳回重复`,possible_duplicate:`标记可能重复`,basis_clear:`清除依据标注`,basis_review:`复核目标依据`,dedup:`目标去重判断`,auto_review:`每日维护自动复核`,reopen:`重开目标`,merge_undo:`撤销自动合并`},Cn={"goal created":`创建目标`,"goal updated":`更新目标`,"goals merged":`合并目标`,"possible duplicate identified":`发现可能重复的目标`,"possible duplicate dismissed":`驳回可能重复关系`,"goal basis reviewed":`复核目标依据`,"basis warning cleared by administrator":`管理员清除依据标注`,"goal reopened by administrator":`管理员重开目标`,auto_completed:`每日维护按依据自动完成`,auto_abandoned:`每日维护按依据自动放弃`,auto_merged:`每日维护补判为重复并合并`,auto_different:`每日维护补判为不重复`,admin_not_merged:`补判为重复,含管理员目标未合并`,"automatic merge undone by administrator":`管理员撤销自动合并`},wn={possible:`可能重复`,dismissed:`已驳回`,merged:`已合并`,active:`生效`,cleared:`已清除`,superseded:`已被新标注替代`,resolved:`依据已恢复或移除`};function Tn({field:e,value:t,catalog:n}){return t===void 0?(0,x.jsx)(x.Fragment,{children:`未记录`}):t===null?(0,x.jsx)(x.Fragment,{children:e===`reminder_minutes`?`使用默认值`:`未设置`}):(e===`deadline`||e===`closed_at`)&&typeof t==`string`?(0,x.jsx)(N,{value:t}):e===`reminder_minutes`?(0,x.jsxs)(x.Fragment,{children:[String(t),` 分钟`]}):e===`people`&&Array.isArray(t)?(0,x.jsx)(x.Fragment,{children:t.map(e=>yt(String(e),n)).join(`、`)||`无`}):e===`entry_id`?(0,x.jsx)(x.Fragment,{children:bt(String(t),n)}):e===`state`?(0,x.jsx)(x.Fragment,{children:pt[t]||String(t)}):e===`kind`?(0,x.jsx)(x.Fragment,{children:mt[t]||String(t)}):e===`origin`?(0,x.jsx)(x.Fragment,{children:ht[t]||String(t)}):e===`closed_by`?(0,x.jsx)(x.Fragment,{children:Se[String(t)]||String(t)}):Array.isArray(t)?(0,x.jsx)(x.Fragment,{children:t.map(e=>typeof e==`object`&&e&&`memory_id`in e?`记忆 #${e.memory_id}(依据修订 ${e.memory_revision})`:`#${e}`).join(`、`)||`无`}):typeof t==`object`?(0,x.jsx)(x.Fragment,{children:Object.entries(t).map(([e,t])=>`#${e}:${wn[String(t)]||String(t)}`).join(`;`)||`无`}):(0,x.jsx)(x.Fragment,{children:String(t)})}function En({id:e,catalog:t}){let[n,r]=(0,l.useState)(0),[i,a]=(0,l.useState)(0),o=y(`/goals/${e}/revisions?limit=30&offset=${n}`),s=y(`/operations?object_type=goal&object_id=${e}&limit=30&offset=${i}`);return(0,x.jsxs)(`section`,{className:`panel goals-panel`,children:[(0,x.jsxs)(`div`,{className:`panel-heading`,children:[(0,x.jsx)(`h2`,{children:`修订与操作记录`}),(0,x.jsx)(`button`,{className:`text-button`,onClick:()=>{o.refresh(),s.refresh()},children:`刷新目标记录`})]}),(0,x.jsxs)(`section`,{"aria-label":`修订历史`,children:[(0,x.jsx)(`h3`,{children:`修订历史`}),(0,x.jsx)(`p`,{className:`goal-help`,children:`每次仅展示发生变化的字段;依据标注和重复关系的变化也会留下修订。`}),o.error&&(0,x.jsxs)(A,{error:!0,children:[o.error,(0,x.jsx)(`button`,{onClick:o.refresh,children:`重试修订历史`})]}),!o.data&&!o.error&&(0,x.jsx)(`p`,{role:`status`,children:`正在读取修订历史…`}),o.data?.history_status===`pre_migration_no_snapshot`&&(0,x.jsxs)(A,{children:[`迁移前无快照:截至修订 `,o.data.missing_through_revision,` `,`的历史值无法还原。`]}),o.data?.items.length===0&&(0,x.jsx)(`p`,{className:`goal-help`,children:`暂无修订快照。`}),o.data?.items.map(e=>(0,x.jsxs)(`article`,{className:`goal-operation`,children:[(0,x.jsxs)(`h4`,{children:[Sn[e.action]||xe[`goal_${e.action}`]||e.action,` `,`·`,` `,e.revision_before==null?`新建`:`修订 ${e.revision_before}`,` `,`→ `,e.revision_after]}),(0,x.jsxs)(`p`,{children:[(0,x.jsx)(N,{value:e.created_at}),` ·`,` `,Se[e.actor]||e.actor]}),(0,x.jsx)(`p`,{className:`report-text`,children:Cn[e.reason]||e.reason}),(0,x.jsx)(`dl`,{className:`revision-values`,children:Array.from(new Set([...Object.keys(e.before),...Object.keys(e.after)])).map(n=>(0,x.jsxs)(`div`,{children:[(0,x.jsx)(`dt`,{children:xn[n]||n}),(0,x.jsxs)(`dd`,{children:[(0,x.jsx)(`span`,{className:`muted`,children:`修改前`}),(0,x.jsx)(`p`,{children:(0,x.jsx)(Tn,{field:n,value:e.before[n],catalog:t})})]}),(0,x.jsxs)(`dd`,{children:[(0,x.jsx)(`span`,{className:`muted`,children:`修改后`}),(0,x.jsx)(`p`,{children:(0,x.jsx)(Tn,{field:n,value:e.after[n],catalog:t})})]})]},n))})]},e.id)),o.data&&(0,x.jsx)(T,{total:o.data.total,offset:n,change:r})]}),(0,x.jsxs)(`details`,{className:`goal-operations`,children:[(0,x.jsx)(`summary`,{children:`操作记录`}),s.error&&(0,x.jsx)(A,{error:!0,children:s.error}),!s.data&&!s.error&&(0,x.jsx)(`p`,{role:`status`,children:`正在读取操作记录…`}),s.data?.items.length===0&&(0,x.jsx)(`p`,{className:`goal-help`,children:`暂无操作记录。`}),s.data?.items.map(e=>(0,x.jsxs)(`article`,{className:`goal-operation`,children:[(0,x.jsx)(`h3`,{children:xe[e.action]||e.action}),(0,x.jsxs)(`p`,{children:[(0,x.jsx)(N,{value:e.created_at}),` ·`,` `,Se[e.actor]||e.actor]}),typeof e.details.revision_before==`number`&&(0,x.jsxs)(`p`,{children:[`修改前修订 `,e.details.revision_before]}),Array.isArray(e.details.fields)&&(0,x.jsxs)(`p`,{children:[`修改字段:`,e.details.fields.map(e=>xn[String(e)]||String(e)).join(`、`)]}),typeof e.details.merged_id==`number`&&(0,x.jsxs)(`p`,{children:[`合并进来的目标 #`,e.details.merged_id]}),typeof e.details.other_id==`number`&&(0,x.jsxs)(`p`,{children:[`驳回与目标 #`,e.details.other_id,` 的可能重复关系`]}),e.action===`goal_create`&&(0,x.jsx)(`p`,{children:`已创建目标。`})]},e.id)),s.data&&(0,x.jsx)(T,{total:s.data.total,offset:i,change:a})]}),(0,x.jsx)(`a`,{className:`text-button`,href:`#/operations?object_type=goal&object_id=${e}`,children:`查看完整操作记录`})]})}function Dn({goal:e,otherId:t,locked:n,catalog:r,openGoal:i,merge:a,dismiss:o}){let s=y(`/goals/${t}`);return(0,x.jsxs)(`article`,{className:`goal-duplicate`,children:[(0,x.jsxs)(`h3`,{children:[`可能重复的目标 #`,t]}),s.error&&(0,x.jsxs)(A,{error:!0,children:[s.error,(0,x.jsx)(`button`,{onClick:s.refresh,children:`重试读取重复目标`})]}),!s.data&&!s.error&&(0,x.jsx)(`p`,{children:`正在读取…`}),s.data&&(0,x.jsxs)(x.Fragment,{children:[(0,x.jsx)(`p`,{className:`goal-content`,children:s.data.content}),(0,x.jsx)(xt,{goal:s.data}),(0,x.jsx)(Ct,{goal:s.data,catalog:r}),(0,x.jsxs)(`div`,{className:`actions`,children:[(0,x.jsxs)(`button`,{className:`secondary`,onClick:()=>i(t),children:[`查看目标 #`,t]}),(0,x.jsxs)(`button`,{disabled:n||s.loading||!!s.error||e.state!==`open`||s.data.state!==`open`||!!s.data.merged_into,onClick:()=>a(s.data),children:[`合并目标 #`,t]}),(0,x.jsxs)(`button`,{disabled:n||s.loading||!!s.error||!!s.data.merged_into,onClick:()=>o(s.data),children:[`驳回重复 #`,t]})]})]})]})}function On({id:e,catalog:t,openMemory:n,openGoal:r,onBack:i,onChange:a}){let[o,s]=(0,l.useState)(null),[c,u]=(0,l.useState)(null),[d,p]=(0,l.useState)(!1),[m,h]=(0,l.useState)(!1),[b,S]=(0,l.useState)(``),[C,w]=(0,l.useState)(``),T=y(`/goals/${e}`,o||c||m?0:2e3),D=T.data,O=d||m||T.loading||!!T.error;function k(){s(null),u(null),S(``),w(``),h(!1),T.refresh()}async function j(e,t,n=`PATCH`){if(!(d||m)){p(!0),S(``),w(``);try{let i=await g(e,_(n,t));s(null),u(null),T.refresh(),a(),e.endsWith(`/merge`)?(w(`已合并,以下为保留目标。`),r(i.id)):w(e.endsWith(`/dismiss`)?`已驳回可能重复关系`:e.endsWith(`/reopen`)?`目标已重开`:e.endsWith(`/undo`)?`已撤销自动合并`:`目标已更新`)}catch(e){S(v(e)),e instanceof f&&e.status===409&&(h(!0),u(null))}finally{p(!1)}}}return(0,x.jsxs)(`div`,{className:`goal-detail`,children:[(0,x.jsxs)(`div`,{className:`actions`,children:[(0,x.jsx)(`button`,{className:`text-button`,disabled:d,onClick:i,children:`← 返回目标列表`}),(0,x.jsx)(`button`,{className:`secondary`,disabled:d,onClick:k,children:m?`载入最新目标`:`刷新目标详情`})]}),!c&&(b||T.error)&&(0,x.jsxs)(A,{error:!0,children:[b||T.error,m&&`。请载入最新目标后重新核对;重新加载会放弃当前编辑草稿。`]}),C&&(0,x.jsx)(A,{children:C}),!D&&!T.error&&(0,x.jsx)(`p`,{role:`status`,children:`正在读取目标详情…`}),D&&(0,x.jsxs)(x.Fragment,{children:[(0,x.jsxs)(`section`,{className:`panel goals-panel`,children:[(0,x.jsxs)(`div`,{className:`panel-heading`,children:[(0,x.jsxs)(`h2`,{children:[`目标 #`,D.id]}),(0,x.jsxs)(`span`,{className:`muted`,children:[`修订 `,D.revision]})]}),(0,x.jsx)(xt,{goal:D}),D.merged_into&&(0,x.jsxs)(A,{children:[`此目标已合并到 #`,D.merged_into,`,原记录与依据保留。`,(0,x.jsxs)(`button`,{className:`text-button`,onClick:()=>r(D.merged_into),children:[`查看保留目标 #`,D.merged_into]})]}),o?(0,x.jsx)(Lt,{goal:o,catalog:t,busy:d,locked:m||!!T.error,cancel:()=>s(null),submit:t=>void j(`/goals/${e}`,{...t,expected_revision:o.revision})}):(0,x.jsxs)(x.Fragment,{children:[(0,x.jsx)(`p`,{className:`goal-content goal-detail-content`,children:D.content}),(0,x.jsx)(Ct,{goal:D,catalog:t}),(0,x.jsxs)(`div`,{className:`goal-time-lines`,children:[(0,x.jsxs)(`p`,{children:[`创建时间:`,(0,x.jsx)(N,{value:D.created_at})]}),(0,x.jsxs)(`p`,{children:[`最后更新:`,(0,x.jsx)(N,{value:D.updated_at})]}),D.state!==`open`&&(0,x.jsxs)(`p`,{children:[`结束时间:`,(0,x.jsx)(N,{value:D.closed_at}),` ·`,` `,D.closed_by?Se[D.closed_by]||D.closed_by:`操作者未知`]})]}),D.overdue&&(0,x.jsx)(`p`,{className:`goal-help`,children:`已过期只是标记,目标仍未结束。可修改截止时间,或明确完成、放弃。`}),!D.merged_into&&(0,x.jsxs)(`div`,{className:`actions`,children:[(0,x.jsx)(`button`,{className:`secondary`,disabled:O,onClick:()=>{s(D),w(``)},children:`编辑目标`}),D.state===`open`&&(0,x.jsxs)(x.Fragment,{children:[(0,x.jsx)(`button`,{disabled:O,onClick:()=>u({kind:`completed`,goal:D}),children:`完成目标`}),(0,x.jsx)(`button`,{className:`danger-text`,disabled:O,onClick:()=>u({kind:`abandoned`,goal:D}),children:`放弃目标`})]})]})]})]}),(0,x.jsx)(Dt,{goal:D,locked:O||!!o||!!c,openMemory:n,clear:e=>u({kind:`basis_clear`,goal:D,annotation:e})}),(0,x.jsx)(Nt,{goal:D,locked:O||!!o||!!c,openGoal:r,reopen:()=>u({kind:`reopen`,goal:D}),undoMerge:e=>u({kind:`undo_merge`,goal:D,judgment:e})}),(0,x.jsx)(Ot,{goal:D,openGoal:r}),!D.merged_into&&D.possible_duplicate_ids.length>0&&(0,x.jsxs)(`section`,{className:`panel goals-panel`,children:[(0,x.jsx)(`h2`,{children:`可能重复`}),(0,x.jsx)(`p`,{className:`goal-help`,children:`相似不代表同一件事。核对双方内容、人物和时间后,再决定合并或驳回。`}),D.possible_duplicate_ids.map(n=>(0,x.jsx)(Dn,{goal:D,otherId:n,locked:O||!!o,catalog:t,openGoal:r,merge:e=>u({kind:`merge`,goal:D,other:e}),dismiss:t=>void j(`/goals/${e}/duplicates/${t.id}/dismiss`,{expected_revision:D.revision,other_revision:t.revision},`POST`)},`${D.revision}-${n}`))]}),(0,x.jsx)(bn,{goal:D,catalog:t,openMemory:n},`sources-${D.revision}`),(0,x.jsxs)(`section`,{className:`panel goals-panel`,children:[(0,x.jsx)(`h2`,{children:`同证据的承诺记忆`}),(0,x.jsx)(`p`,{className:`goal-help`,children:`与目标使用同一来源消息的角色记忆,保留当时依据修订。目标完成不会自动改写记忆。`}),!D.promise_memories.length&&(0,x.jsx)(`p`,{children:`暂无关联记忆。`}),D.promise_memories.map(e=>(0,x.jsxs)(`article`,{className:`goal-operation`,children:[(0,x.jsx)(`p`,{className:`goal-content`,children:e.content}),(0,x.jsxs)(`p`,{className:`muted`,children:[`依据修订 `,e.revision,` · 当前修订`,` `,e.current_revision,` · `,E(e.lifecycle),e.revision===e.current_revision?``:` · 依据已变化`]}),(0,x.jsxs)(`button`,{className:`text-button`,onClick:()=>n(e.id),children:[`查看记忆 #`,e.id]})]},e.id))]}),(0,x.jsxs)(`section`,{className:`panel goals-panel`,children:[(0,x.jsx)(`h2`,{children:`合并进来的目标`}),!D.merged_goals.length&&(0,x.jsx)(`p`,{className:`goal-help`,children:`暂无合并记录。`}),D.merged_goals.map(e=>(0,x.jsxs)(`article`,{className:`goal-operation`,children:[(0,x.jsx)(`p`,{className:`goal-content`,children:e.content}),(0,x.jsx)(Ct,{goal:e,catalog:t}),(0,x.jsxs)(`button`,{className:`text-button`,onClick:()=>r(e.id),children:[`查看原目标 #`,e.id]})]},e.id))]}),(0,x.jsx)(zt,{goalId:D.id,openGoal:r}),(0,x.jsx)(`section`,{className:`panel goals-panel`,children:(0,x.jsxs)(`details`,{children:[(0,x.jsxs)(`summary`,{children:[`提醒计划 · `,D.reminder_plans.length,` 条`]}),(0,x.jsx)(`p`,{className:`goal-help`,children:`计划到点才发布提醒,尚未发布的计划不属于“待取走”提醒。`}),D.reminder_plans.map(e=>(0,x.jsxs)(`div`,{className:`goal-operation`,children:[(0,x.jsxs)(`p`,{children:[gt[e.reminder_kind],` ·`,` `,{scheduled:`待发布`,published:`已发布`,skipped:`已跳过`,cancelled:`已取消`}[e.status]||e.status]}),(0,x.jsx)(N,{value:e.scheduled_at})]},e.id))]})}),(0,x.jsx)(En,{id:D.id,catalog:t},`operations-${D.revision}`)]}),c?.kind===`reopen`&&(0,x.jsx)(Pt,{goal:c.goal,busy:d||m,error:b,cancel:()=>u(null),confirm:()=>void j(`/goals/${e}/reopen`,{expected_revision:c.goal.revision},`POST`)}),c?.kind===`undo_merge`&&(0,x.jsx)(jt,{judgment:c.judgment,busy:d||m,error:b,cancel:()=>u(null),confirm:()=>void j(`/goal-duplicate-judgments/${c.judgment.id}/undo`,{expected_revision:c.judgment.target_revision},`POST`)}),c&&c.kind!==`reopen`&&c.kind!==`undo_merge`&&(0,x.jsxs)(ee,{title:c.kind===`basis_clear`?`清除目标依据标注`:c.kind===`merge`?`确认合并目标`:c.kind===`completed`?`完成目标`:`放弃目标`,closeDisabled:d,onClose:()=>u(null),children:[b&&(0,x.jsx)(A,{error:!0,children:b}),c.kind===`basis_clear`?(0,x.jsxs)(`div`,{className:`goal-confirm`,children:[(0,x.jsx)(`p`,{children:c.annotation.text}),(0,x.jsx)(`p`,{children:`清除只撤下这次观察的标注并保留修订记录,不改变目标正文、状态、截止时间或提醒;依据再次变化时仍可能出现新标注。`}),(0,x.jsxs)(`div`,{className:`actions`,children:[(0,x.jsx)(`button`,{className:`primary`,disabled:d||m,onClick:()=>void j(`/goals/${e}/basis-annotations/${c.annotation.id}`,{expected_revision:c.goal.revision},`DELETE`),children:`确认清除依据标注`}),(0,x.jsx)(`button`,{disabled:d,onClick:()=>u(null),children:`取消`})]})]}):c.kind===`merge`?(0,x.jsxs)(`div`,{className:`goal-confirm`,children:[(0,x.jsxs)(`p`,{children:[`保留较早的目标 #`,[c.goal,c.other].sort((e,t)=>Date.parse(e.created_at)-Date.parse(t.created_at)||e.id-t.id)[0].id,`,另一条保留为指向它的合并记录。`]}),(0,x.jsx)(`p`,{children:`来源与待取提醒归入保留目标,重复提醒可能取消;已取走的记录仍保留。保留目标的正文不自动拼接;明确截止时间补入空值,两方都设提前量时采用较大的分钟数。`}),[c.goal,c.other].map(e=>(0,x.jsxs)(`div`,{children:[(0,x.jsxs)(`h3`,{children:[`目标 #`,e.id,` · 修订 `,e.revision]}),(0,x.jsx)(`p`,{className:`goal-content`,children:e.content}),(0,x.jsx)(Ct,{goal:e,catalog:t})]},e.id)),(0,x.jsxs)(`div`,{className:`actions`,children:[(0,x.jsx)(`button`,{className:`primary`,disabled:d||m,onClick:()=>void j(`/goals/${e}/duplicates/${c.other.id}/merge`,{expected_revision:c.goal.revision,other_revision:c.other.revision},`POST`),children:`确认合并`}),(0,x.jsx)(`button`,{className:`secondary`,disabled:d,onClick:()=>u(null),children:`取消`})]})]}):(0,x.jsxs)(`div`,{className:`goal-confirm`,children:[(0,x.jsx)(`p`,{children:c.goal.content}),(0,x.jsxs)(`p`,{children:[`确认`,c.kind===`completed`?`完成`:`放弃`,`后,尚未取走的提醒会取消,已取走的记录保留。这是明确改变目标状态。`]}),(0,x.jsxs)(`div`,{className:`actions`,children:[(0,x.jsx)(`button`,{className:`primary`,disabled:d||m,onClick:()=>void j(`/goals/${e}`,{state:c.kind,expected_revision:c.goal.revision}),children:c.kind===`completed`?`确认完成`:`确认放弃`}),(0,x.jsx)(`button`,{className:`secondary`,disabled:d,onClick:()=>u(null),children:`取消`})]})]})]})]})}var kn={state:`open`,kind:``,overdue:``,due_soon:``,possible_duplicate:``,entry_id:``,deadline_from:``,deadline_to:``},An=e=>new URLSearchParams(Object.entries(e).filter(([,e])=>e!==``)).toString();function jn({initialId:e=null,openMemory:t}){let n=y(`/catalog`),[r,i]=(0,l.useState)(e),[a,o]=(0,l.useState)(kn),[s,c]=(0,l.useState)(An(kn)),[u,d]=(0,l.useState)(0),[f,p]=(0,l.useState)(``),[m,h]=(0,l.useState)(!1),[b,S]=(0,l.useState)(!1),[C,w]=(0,l.useState)(``),[E,D]=(0,l.useState)(null),O=y(r===null?`/goals?${s}&limit=30&offset=${u}`:null,u===0?2e3:0),k=(e,t)=>o(n=>({...n,[e]:t}));function M(e){if(e.preventDefault(),a.deadline_from&&a.deadline_to&&a.deadline_from>a.deadline_to){p(`开始日期不能晚于结束日期`);return}p(``),d(0),c(An(a)),O.refresh()}async function N(e){if(!b){S(!0),w(``);try{let t=await g(`/goals`,_(`POST`,e));D(t),h(!1),O.refresh()}catch(e){w(v(e))}finally{S(!1)}}}return(0,x.jsxs)(`div`,{className:`goal-workspace`,children:[n.error&&(0,x.jsxs)(A,{error:!0,children:[`人物与入口名称读取失败:`,n.error,(0,x.jsx)(`button`,{className:`text-button`,onClick:n.refresh,children:`重试人物与入口`})]}),r===null?(0,x.jsxs)(x.Fragment,{children:[(0,x.jsxs)(`section`,{className:`panel goals-panel`,children:[(0,x.jsxs)(`div`,{className:`panel-heading`,children:[(0,x.jsx)(`h2`,{children:`目标与询问`}),(0,x.jsx)(`button`,{className:`primary`,onClick:()=>{w(``),h(!0)},children:`创建目标或询问`})]}),(0,x.jsx)(`p`,{className:`goal-help`,children:`所有入口共享目标。管理员创建的目标不自动合并,相似项仅标为“可能重复”。已过期只是标记,需要明确完成或放弃。`}),(0,x.jsxs)(`form`,{className:`goal-filters`,onSubmit:M,children:[(0,x.jsxs)(`label`,{children:[`目标状态`,(0,x.jsxs)(`select`,{value:a.state,onChange:e=>k(`state`,e.target.value),children:[(0,x.jsx)(`option`,{value:``,children:`全部状态`}),Object.entries(pt).map(([e,t])=>(0,x.jsx)(`option`,{value:e,children:t},e))]})]}),(0,x.jsxs)(`label`,{children:[`目标类型`,(0,x.jsxs)(`select`,{value:a.kind,onChange:e=>k(`kind`,e.target.value),children:[(0,x.jsx)(`option`,{value:``,children:`全部类型`}),Object.entries(mt).map(([e,t])=>(0,x.jsx)(`option`,{value:e,children:t},e))]})]}),[[`overdue`,`是否过期`],[`due_soon`,`是否临近`],[`possible_duplicate`,`是否可能重复`]].map(([e,t])=>(0,x.jsxs)(`label`,{children:[t,(0,x.jsxs)(`select`,{value:a[e],onChange:t=>k(e,t.target.value),children:[(0,x.jsx)(`option`,{value:``,children:`全部`}),(0,x.jsx)(`option`,{value:`true`,children:`是`}),(0,x.jsx)(`option`,{value:`false`,children:`否`})]})]},e)),(0,x.jsxs)(`label`,{children:[`产生的入口`,(0,x.jsxs)(`select`,{value:a.entry_id,onChange:e=>k(`entry_id`,e.target.value),children:[(0,x.jsx)(`option`,{value:``,children:`全部入口`}),n.data?.entries.map(e=>(0,x.jsx)(`option`,{value:e.id,children:e.name||e.id},e.id))]})]}),(0,x.jsxs)(`label`,{children:[`截止日期从`,(0,x.jsx)(`input`,{type:`date`,value:a.deadline_from,onChange:e=>k(`deadline_from`,e.target.value)})]}),(0,x.jsxs)(`label`,{children:[`截止日期至`,(0,x.jsx)(`input`,{type:`date`,value:a.deadline_to,onChange:e=>k(`deadline_to`,e.target.value)})]}),(0,x.jsx)(`p`,{className:`goal-help full-width`,children:`日期范围按角色时区解释,结束日期包含当天。临近范围包含已过期目标;选择“是否过期:否”可排除。`}),(0,x.jsxs)(`div`,{className:`actions full-width`,children:[(0,x.jsx)(`button`,{className:`primary`,children:`筛选目标`}),(0,x.jsx)(`button`,{type:`button`,className:`secondary`,onClick:O.refresh,children:`刷新目标`}),(0,x.jsx)(`button`,{type:`button`,className:`text-button`,onClick:()=>{o(kn),c(An(kn)),d(0),p(``),O.refresh()},children:`重置筛选`})]})]})]}),E&&(0,x.jsx)(wt,{receipt:E,openGoal:i}),(f||O.error)&&(0,x.jsxs)(A,{error:!0,children:[f||O.error,O.error&&O.data&&`。以下为上次读取结果,可能尚未更新。`]}),(0,x.jsx)(`div`,{className:`list-heading`,children:(0,x.jsx)(`span`,{children:O.data?`共 ${O.data.total} 个目标与询问`:O.error?`目标暂不可读取`:`正在读取目标…`})}),O.data?.items.length===0&&(0,x.jsx)(j,{title:`当前范围没有目标或询问`,children:`可以创建目标,或在试用对话中让角色答应一件事,学习后在这里查看。`}),(0,x.jsx)(`div`,{className:`goal-grid`,children:O.data?.items.map(e=>(0,x.jsxs)(`article`,{className:`panel goals-panel`,children:[(0,x.jsx)(xt,{goal:e}),(0,x.jsx)(`h3`,{className:`goal-content`,children:e.content}),(0,x.jsx)(Ct,{goal:e,catalog:n.data}),(0,x.jsxs)(`button`,{className:`text-button`,onClick:()=>i(e.id),children:[`查看目标 #`,e.id]})]},e.id))}),O.data&&(0,x.jsx)(T,{total:O.data.total,offset:u,change:d}),(0,x.jsx)(Ft,{openGoal:i,onChange:O.refresh})]}):(0,x.jsx)(On,{id:r,catalog:n.data,openMemory:t,openGoal:i,onBack:()=>i(null),onChange:O.refresh},r),m&&(0,x.jsxs)(ee,{title:`创建目标或询问`,onClose:()=>h(!1),closeDisabled:b,children:[(0,x.jsx)(`p`,{className:`goal-help`,children:`手动创建只会标记可能重复,保留每一条目标供你核对。`}),C&&(0,x.jsx)(A,{error:!0,children:C}),(0,x.jsx)(Lt,{catalog:n.data,busy:b,submit:e=>void N(e),cancel:()=>h(!1)})]})]})}var Mn={host:`宿主提供`,first_report:`自首次报告起`},Nn={start:`开始活动`,replace:`更换活动`,update:`更新状态`,heartbeat:`心跳`,end:`结束活动`},Pn={activity:`活动`,mood:`情绪`,started_at:`开始时间`,start_time_basis:`开始时间依据`};function Fn(e){let t=Math.max(0,Math.floor(e));return[[Math.floor(t/86400),`天`],[Math.floor(t/3600)%24,`小时`],[Math.floor(t/60)%60,`分钟`],[t%60,`秒`]].filter(([e])=>e!==0).map(([e,t])=>`${e} ${t}`).join(` `)||`0 秒`}function In(e,t=!1){return e===null?`无`:typeof e==`boolean`?e?`是(true)`:`否(false)`:e===``?`空文本`:t&&typeof e==`string`?`“${e}”`:String(e)}function Ln({state:e}){return(0,x.jsx)(x.Fragment,{children:e.mood===null?e.mood_updated_at?`已清空`:`未报告`:In(e.mood)})}function Rn({error:e,retained:t}){return e?(0,x.jsxs)(A,{error:!0,children:[`当前状态读取失败:`,e,`。`,t&&`以下为上次读取结果,可能尚未更新。`]}):null}function zn({state:e}){return(0,x.jsxs)(x.Fragment,{children:[e.possibly_stale&&(0,x.jsxs)(`div`,{className:`state-stale`,children:[(0,x.jsx)(M,{tone:`warning`,children:`可能过时`}),(0,x.jsxs)(`p`,{children:[`超过 `,e.stale_after_minutes,` `,`分钟未收到报告,保留宿主最后报告的活动。`]})]}),(0,x.jsxs)(`dl`,{className:`state-fields`,children:[(0,x.jsxs)(`div`,{className:`full-width`,children:[(0,x.jsx)(`dt`,{children:`当前活动`}),(0,x.jsx)(`dd`,{className:`state-activity`,children:e.activity}),(0,x.jsxs)(`dd`,{className:`state-field-time`,children:[`活动更新时间:`,(0,x.jsx)(N,{value:e.activity_updated_at})]})]}),(0,x.jsxs)(`div`,{children:[(0,x.jsx)(`dt`,{children:`情绪`}),(0,x.jsx)(`dd`,{children:(0,x.jsx)(Ln,{state:e})}),(0,x.jsxs)(`dd`,{className:`state-field-time`,children:[`情绪更新时间:`,(0,x.jsx)(N,{value:e.mood_updated_at})]})]}),(0,x.jsxs)(`div`,{children:[(0,x.jsx)(`dt`,{children:`活动持续时长`}),(0,x.jsx)(`dd`,{children:Fn(e.duration_seconds)})]}),(0,x.jsxs)(`div`,{children:[(0,x.jsx)(`dt`,{children:`开始时间`}),(0,x.jsx)(`dd`,{children:(0,x.jsx)(N,{value:e.started_at})})]}),(0,x.jsxs)(`div`,{children:[(0,x.jsx)(`dt`,{children:`开始时间依据`}),(0,x.jsx)(`dd`,{children:Mn[e.start_time_basis]})]}),(0,x.jsxs)(`div`,{className:`full-width`,children:[(0,x.jsx)(`dt`,{children:`最后更新时间`}),(0,x.jsx)(`dd`,{children:(0,x.jsx)(N,{value:e.updated_at})})]}),(0,x.jsxs)(`div`,{children:[(0,x.jsx)(`dt`,{children:`最后报告宿主`}),(0,x.jsx)(`dd`,{children:e.host??`未知`})]}),(0,x.jsxs)(`div`,{children:[(0,x.jsx)(`dt`,{children:`最后报告入口`}),(0,x.jsx)(`dd`,{children:e.entry_id??`未知`})]})]}),(0,x.jsx)(`h3`,{children:`细节`}),Object.keys(e.details).length?(0,x.jsx)(`dl`,{className:`state-fields state-detail-fields`,children:Object.entries(e.details).map(([e,t])=>(0,x.jsxs)(`div`,{children:[(0,x.jsx)(`dt`,{children:e}),(0,x.jsx)(`dd`,{children:In(t.value)}),(0,x.jsxs)(`dd`,{className:`state-field-time`,children:[`更新时间:`,(0,x.jsx)(N,{value:t.updated_at})]})]},e))}):(0,x.jsx)(`p`,{className:`quiet`,children:`暂无细节报告`})]})}function Bn(){let e=y(`/state`,2e3),t=e.data;return(0,x.jsxs)(`div`,{className:`state-summary`,children:[(0,x.jsx)(Rn,{error:e.error,retained:t!==null}),e.error&&(0,x.jsx)(`button`,{className:`text-button`,onClick:e.refresh,children:`重试当前状态`}),!t&&!e.error&&(0,x.jsx)(`p`,{role:`status`,children:`正在读取当前状态…`}),t&&(t.activity?(0,x.jsxs)(x.Fragment,{children:[(0,x.jsx)(`p`,{className:`state-summary-activity`,children:t.activity}),t.possibly_stale&&(0,x.jsx)(M,{tone:`warning`,children:`可能过时`}),(0,x.jsxs)(`dl`,{className:`state-summary-fields`,children:[(0,x.jsxs)(`div`,{children:[(0,x.jsx)(`dt`,{children:`情绪`}),(0,x.jsx)(`dd`,{children:(0,x.jsx)(Ln,{state:t})})]}),(0,x.jsxs)(`div`,{children:[(0,x.jsx)(`dt`,{children:`活动持续时长`}),(0,x.jsx)(`dd`,{children:Fn(t.duration_seconds)})]})]}),t.start_time_basis===`first_report`&&(0,x.jsx)(`p`,{className:`muted`,children:`持续时长自首次报告起计算。`})]}):(0,x.jsx)(`p`,{className:`quiet`,children:`暂无宿主报告的当前活动`})),(0,x.jsx)(`a`,{className:`text-button`,href:`#/state`,children:`查看状态与报告历史`})]})}function Vn({report:e}){let t=[...Object.entries(Pn).flatMap(([t,n])=>{let r=e.changes[t];return r?[{key:t,label:n,...r}]:[]}),...Object.entries(e.changes.details||{}).map(([e,t])=>({key:`details.${e}`,label:`细节 · ${e}`,...t}))],n=(e,t)=>t===null?`无`:e===`started_at`?(0,x.jsx)(N,{value:String(t)}):e===`start_time_basis`?Mn[t]||String(t):In(t,!0);return(0,x.jsxs)(`article`,{className:`state-report`,"aria-label":`报告 #${e.id}`,children:[(0,x.jsxs)(`div`,{className:`panel-heading`,children:[(0,x.jsxs)(`h3`,{children:[Nn[e.action],` · `,e.action]}),(0,x.jsxs)(`span`,{className:`muted`,children:[`报告 #`,e.id,` · `,e.method]})]}),(0,x.jsx)(`p`,{className:`state-report-time`,children:(0,x.jsx)(N,{value:e.reported_at})}),(0,x.jsxs)(`dl`,{className:`state-report-source`,children:[(0,x.jsxs)(`div`,{children:[(0,x.jsx)(`dt`,{children:`宿主`}),(0,x.jsx)(`dd`,{children:e.host??`未知`})]}),(0,x.jsxs)(`div`,{children:[(0,x.jsx)(`dt`,{children:`入口`}),(0,x.jsx)(`dd`,{children:e.entry_id??`未知`})]})]}),t.length?(0,x.jsx)(`dl`,{className:`state-changes`,children:t.map(e=>(0,x.jsxs)(`div`,{children:[(0,x.jsx)(`dt`,{children:e.label}),(0,x.jsxs)(`dd`,{children:[(0,x.jsx)(`span`,{className:`muted`,children:`原值`}),(0,x.jsx)(`span`,{children:n(e.key,e.before)})]}),(0,x.jsxs)(`dd`,{children:[(0,x.jsx)(`span`,{className:`muted`,children:`现值`}),(0,x.jsx)(`span`,{children:n(e.key,e.after)})]})]},e.key))}):(0,x.jsx)(`p`,{className:`muted`,children:`无字段值变化,本次报告仍已记录。`})]})}function Hn(){let e=y(`/state`,2e3),[t,n]=(0,l.useState)(0),r=y(`/state/reports?limit=30&offset=${t}`,t===0?2e3:0);return(0,x.jsx)(x.Fragment,{children:(0,x.jsxs)(`div`,{className:`state-page`,children:[(0,x.jsxs)(`section`,{className:`panel state-panel`,"aria-labelledby":`current-state-title`,children:[(0,x.jsxs)(`div`,{className:`panel-heading`,children:[(0,x.jsx)(`h2`,{id:`current-state-title`,children:`当前状态`}),(0,x.jsx)(`button`,{className:`text-button`,disabled:e.loading,onClick:e.refresh,children:`刷新当前状态`})]}),(0,x.jsx)(`p`,{className:`state-help`,children:`活动持续时长与情绪、细节的更新时间分别记录。时间按角色时区显示,保留 UTC 偏移。`}),(0,x.jsx)(Rn,{error:e.error,retained:e.data!==null}),!e.data&&!e.error&&(0,x.jsx)(`p`,{role:`status`,children:`正在读取当前状态…`}),e.data&&(e.data.activity?(0,x.jsx)(zn,{state:e.data}):(0,x.jsx)(j,{title:`暂无宿主报告的当前活动`,children:`宿主开始活动后会显示在这里;结束活动后,报告历史仍会保留。`}))]}),(0,x.jsxs)(`section`,{className:`panel state-panel`,"aria-labelledby":`state-reports-title`,children:[(0,x.jsxs)(`div`,{className:`panel-heading`,children:[(0,x.jsxs)(`h2`,{id:`state-reports-title`,children:[`报告历史`,r.data?` · ${r.data.total} 条`:``]}),(0,x.jsx)(`button`,{className:`text-button`,disabled:r.loading,onClick:()=>t===0?r.refresh():n(0),children:`刷新报告历史`})]}),(0,x.jsxs)(`p`,{className:`state-help`,children:[`按报告顺序从新到旧排列,列出字段值的实际变化。心跳也会保留记录。`,t===0?`最新一页自动刷新。`:`较早报告暂停自动刷新;刷新历史可回到最新一页。`]}),r.error&&(0,x.jsxs)(A,{error:!0,children:[`报告历史读取失败:`,r.error,`。`,r.data&&`以下为上次读取的报告。`]}),!r.data&&!r.error&&(0,x.jsx)(`p`,{role:`status`,children:`正在读取报告历史…`}),r.data?.items.length===0&&(0,x.jsx)(j,{title:`暂无报告历史`}),r.data?.items.map(e=>(0,x.jsx)(Vn,{report:e},e.id)),r.data&&(0,x.jsx)(T,{total:r.data.total,offset:t,limit:30,change:n})]})]})})}function Un({initialQuery:e=``,openMemory:t=()=>{}}){let n=new URLSearchParams(e),r=Number(n.get(`id`)),[i,a]=(0,l.useState)(Number.isSafeInteger(r)&&r>0?r:null),o=n.get(`tab`),[s,c]=(0,l.useState)(o===`goals`||o===`notifications`?o:`state`);function u(e){a(e),c(`goals`)}return(0,x.jsxs)(x.Fragment,{children:[(0,x.jsx)(`div`,{className:`page-heading`,children:(0,x.jsxs)(`div`,{children:[(0,x.jsx)(`p`,{className:`eyebrow`,children:`此刻与接下来的事`}),(0,x.jsx)(`h1`,{children:`状态与目标`}),(0,x.jsx)(`p`,{children:`当前状态完全由宿主报告,管理员不能编辑。状态与目标由所有入口共享。`})]})}),(0,x.jsxs)(`nav`,{className:`learning-tabs`,"aria-label":`状态与目标分区`,children:[(0,x.jsx)(`button`,{"aria-pressed":s===`state`,onClick:()=>c(`state`),children:`当前状态`}),(0,x.jsx)(`button`,{"aria-pressed":s===`goals`,onClick:()=>{a(null),c(`goals`)},children:`目标与询问`}),(0,x.jsx)(`button`,{"aria-pressed":s===`notifications`,onClick:()=>c(`notifications`),children:`提醒`})]}),s===`state`?(0,x.jsx)(Hn,{}):s===`goals`?(0,x.jsx)(jn,{initialId:i,openMemory:t},i):(0,x.jsx)(zt,{openGoal:u})]})}function Wn({goals:e,error:t=``}){let n=y(`/notifications?status=pending&limit=1&offset=0`,2e3),r=e?.filter(e=>e.state===`open`&&!e.merged_into).slice(0,10);return(0,x.jsxs)(`section`,{className:`panel trial-goals`,"aria-labelledby":`trial-goals-title`,children:[(0,x.jsxs)(`div`,{className:`panel-heading`,children:[(0,x.jsx)(`h2`,{id:`trial-goals-title`,children:`未结束的目标与询问`}),(0,x.jsx)(M,{children:r?.length??`—`})]}),(0,x.jsx)(`p`,{className:`goal-help`,children:`角色答应一件事 → 学习形成目标 → 到点发布提醒。可在学习结束后点击目标查看来源;全部入口共享,优先展示临近、过期的目标,最多 10 个。`}),t&&(0,x.jsxs)(A,{error:!0,children:[`目标暂不可读取:`,t,`。`,r&&`以下为上次读取结果。`]}),!r&&!t&&(0,x.jsx)(`p`,{role:`status`,children:`正在读取目标…`}),r?.length===0&&(0,x.jsx)(`p`,{className:`quiet`,children:`暂无未结束的目标或询问`}),r?.map(e=>(0,x.jsxs)(`a`,{className:`trial-goal-link`,href:`#/state?tab=goals&id=${e.id}`,children:[(0,x.jsx)(xt,{goal:e}),(0,x.jsx)(`p`,{className:`goal-content`,children:e.content}),(0,x.jsx)(`small`,{children:(0,x.jsx)(St,{goal:e})}),(0,x.jsx)(`span`,{className:`text-button`,children:`查看目标与来源 ↗`})]},e.id)),n.error&&(0,x.jsxs)(`p`,{className:`danger-text`,children:[`提醒暂不可读取:`,n.error,(0,x.jsx)(`button`,{className:`text-button`,onClick:n.refresh,children:`重试提醒`})]}),(0,x.jsxs)(`div`,{className:`actions`,children:[(0,x.jsx)(`a`,{className:`text-button`,href:`#/state?tab=goals`,children:`查看全部目标与询问`}),(0,x.jsxs)(`a`,{className:`text-button`,href:`#/state?tab=notifications`,children:[`查看提醒`,n.data?.total===void 0?``:` · ${n.data.total} 条待取走`]})]}),(0,x.jsx)(`p`,{className:`goal-help`,children:`提醒被宿主取走不代表已送达或目标完成。这里的自动刷新不会取走提醒。`})]})}function Gn(e){let t=y(`/trial`),[n,r]=(0,l.useState)(``),[i,a]=(0,l.useState)(!1),[o,s]=(0,l.useState)(``),[c,u]=(0,l.useState)(`group`),[d,f]=(0,l.useState)(``),[p,m]=(0,l.useState)(!1),h=t.data?.entries||[],b=(0,l.useMemo)(()=>{if(!t.data)return null;let e=new Map;for(let n of t.data.speakers){let t=e.get(n.id);e.set(n.id,{...n,is_default:!!(n.is_default||t?.is_default)})}return{...t.data,speakers:[...e.values()]}},[t.data]);(0,l.useEffect)(()=>{!n&&h.length&&r(h[0].id)},[n,h]);async function S(n){n.preventDefault(),m(!0),f(``);try{let n=await g(`/trial/entries`,_(`POST`,{name:o,kind:c}));r(n.id),a(!1),s(``),t.refresh(),e.refreshStatus()}catch(e){f(v(e))}finally{m(!1)}}return(0,x.jsxs)(x.Fragment,{children:[(0,x.jsxs)(`div`,{className:`page-heading`,children:[(0,x.jsxs)(`div`,{children:[(0,x.jsx)(`p`,{className:`eyebrow`,children:`在相处中,慢慢记住`}),(0,x.jsx)(`h1`,{children:`试用对话`}),(0,x.jsx)(`p`,{children:`聊一件小事,看看它如何成为记忆。`})]}),(0,x.jsx)(M,{tone:`purple`,children:h.find(e=>e.id===n)?`${at(h.find(e=>e.id===n).pace)}学习`:`试用入口`})]}),(t.error||d)&&(0,x.jsx)(A,{error:!0,children:t.error||d}),(0,x.jsxs)(`div`,{className:`entry-bar`,children:[(0,x.jsxs)(`label`,{children:[`当前入口`,(0,x.jsxs)(`select`,{"aria-label":`当前入口`,value:n,onChange:e=>r(e.target.value),children:[(0,x.jsx)(`option`,{value:``,disabled:!0,children:`选择试用入口`}),h.map(e=>(0,x.jsx)(`option`,{value:e.id,children:e.name},e.id))]})]}),(0,x.jsx)(`button`,{className:`secondary`,onClick:()=>a(!i),children:`+ 新建入口`}),(0,x.jsx)(`span`,{className:`muted entry-tip`,children:`记忆共同积累,近期消息各自保留`})]}),(i||t.data&&!h.length)&&(0,x.jsxs)(`form`,{className:`inline-form panel`,onSubmit:S,children:[(0,x.jsxs)(`label`,{children:[`入口名称`,(0,x.jsx)(`input`,{value:o,onChange:e=>s(e.target.value),placeholder:`如:试用群聊 A`,required:!0,maxLength:100})]}),(0,x.jsxs)(`label`,{children:[`入口类型`,(0,x.jsxs)(`select`,{value:c,onChange:e=>u(e.target.value),children:[(0,x.jsx)(`option`,{value:`group`,children:`群聊`}),(0,x.jsx)(`option`,{value:`private`,children:`私聊`})]})]}),(0,x.jsx)(`button`,{className:`primary`,disabled:p||!o.trim(),children:`创建入口`})]}),!t.data&&!t.error&&(0,x.jsx)(`p`,{className:`muted`,children:`正在读取试用入口…`}),n&&b&&(0,x.jsx)(Kn,{entryId:n,catalog:b,refreshCatalog:t.refresh,...e},n)]})}function Kn({entryId:e,catalog:t,refreshCatalog:n,status:r,openMemory:i,refreshStatus:a}){let o=y(`/trial/entries/${e}`,2e3),[s,c]=(0,l.useState)(t.speakers.find(e=>e.is_default)?.id||``);(0,l.useEffect)(()=>{s||c(t.speakers.find(e=>e.is_default)?.id||``)},[s,t.speakers]);let[u,d]=(0,l.useState)(!1),[p,m]=(0,l.useState)(``),[h,b]=(0,l.useState)(``),C=`iris.trial.replies.${e}`,[w,T]=(0,l.useState)(()=>{try{return window.localStorage.getItem(C)===`true`}catch{return!1}});function E(e){T(e);try{window.localStorage.setItem(C,String(e))}catch{}}let[D,ee]=(0,l.useState)(!1),[N,P]=(0,l.useState)(``),[F,I]=(0,l.useState)(``),[te,L]=(0,l.useState)(``),[ne,R]=(0,l.useState)(null),[re,ie]=(0,l.useState)(null),[ae,oe]=(0,l.useState)(null),[se,ce]=(0,l.useState)(``),[le,ue]=(0,l.useState)([]),[de,z]=(0,l.useState)(null),[B,V]=(0,l.useState)(!1),[fe,pe]=(0,l.useState)(null),me=Ze(),he=(0,l.useRef)(null);(0,l.useEffect)(()=>()=>he.current?.abort(),[]);function H(e){if(!D){I(``);try{me.add(e)}catch(e){I(v(e))}}}let U=o.data,ge=r?.entries.find(t=>t.entry_id===e),_e=[...new Map([...le,...U?.messages||[]].map(e=>[e.id,e])).values()].sort((e,t)=>e.id-t.id),ve=(0,l.useRef)(null),ye=(0,l.useRef)(!0),be=_e.at(-1)?.id;(0,l.useEffect)(()=>{ye.current&&ve.current&&(ve.current.scrollTop=ve.current.scrollHeight)},[be]);async function xe(e){e.preventDefault(),I(``);try{let e=await g(`/trial/speakers`,_(`POST`,{name:p}));n(),c(e.id),d(!1),m(``)}catch(e){I(v(e))}}async function Se(t,n){P(n?`角色正在回复…`:`正在准备回复材料…`),ce(``),oe(null),ie(null);try{if(n){R(t);let n=await g(`/trial/entries/${e}/reply`,_(`POST`,{message_id:t}));ie(n.prepared),oe(n.diagnostic||null),n.reused&&ce(n.diagnostic?`已复用回复 #${n.diagnostic.reply_message_id};以下诊断来自发布该回复时的准备。`:`已复用已发布回复;原诊断未记录或已按保留期限清理,无法还原当时的判断。`),R(null)}else ie(await g(`/trial/entries/${e}/prepare`,_(`POST`)))}catch(e){let t=e instanceof f&&e.code===`reply_failed`?e.result:void 0;throw t?.diagnostic?(oe(t.diagnostic),ce(`角色回复失败;以下为本次已经完成的回复准备诊断。`)):ce(`本次回复准备结果未返回,无法展示召回判断状态。`),e}}async function Ce(t){if(t.preventDefault(),!h.trim()&&!me.images.length||D||!s)return;let n=new AbortController;he.current=n,ee(!0),I(``),L(``),ie(null),oe(null),ce(``),R(null),P(`正在发送…`);let r=me.images.map(e=>e.key).join(`,`),i=fe?.content===h&&fe.speaker===s&&fe.imageKeys===r?fe:{content:h,speaker:s,imageKeys:r,key:crypto.randomUUID()};pe(i);try{let t=await me.upload(n.signal,P);n.signal.throwIfAborted(),P(`正在发送…`);let r=await g(`/trial/entries/${e}/messages`,{..._(`POST`,{speaker_id:s,content:h,dedupe_key:i.key,media_ids:t}),signal:n.signal});n.signal.throwIfAborted(),b(``),me.clear(),pe(null),L(`消息已接收,学习结果请看右侧批次。`),o.refresh(),a(),await Se(r.message_id,w)}catch(e){n.signal.aborted||I(v(e))}finally{n.signal.aborted||(ee(!1),P(``),o.refresh(),a())}}async function we(){if(ne){ee(!0),I(``);try{await Se(ne,!0)}catch(e){I(v(e))}finally{ee(!1),P(``),o.refresh(),a()}}}async function Te(){I(``);try{let t=await g(`/trial/entries/${e}/learn`,_(`POST`));L(t.paused?`已请求立即学习;模型暂停中,恢复后处理。`:`已请求立即学习,完成情况请看批次状态。`),a()}catch(e){I(v(e))}}async function Ee(){V(!0);try{let t=await g(`/trial/entries/${e}?before=${_e[0].id}`);ue([...t.messages,...le]),z(t.has_older)}catch(e){I(v(e))}finally{V(!1)}}return(0,x.jsxs)(`div`,{className:`trial-grid`,children:[(0,x.jsxs)(`section`,{className:`panel conversation`,children:[(0,x.jsxs)(`div`,{className:`panel-heading`,children:[(0,x.jsxs)(`div`,{children:[(0,x.jsx)(`h2`,{children:U?.entry.name||`正在读取对话…`}),(0,x.jsxs)(`span`,{className:`muted`,children:[U?.entry.kind===`private`?`私聊`:`群聊`,` · 图文试用`]})]}),(0,x.jsxs)(`span`,{className:`live-label`,children:[(0,x.jsx)(`i`,{}),` `,o.error?`连接中断`:U?`入口已就绪`:`正在连接`]})]}),(0,x.jsxs)(`div`,{className:`messages`,"aria-label":`对话记录`,ref:ve,onScroll:e=>{let t=e.currentTarget;ye.current=t.scrollHeight-t.scrollTop-t.clientHeight<60},children:[(de??U?.has_older)&&(0,x.jsx)(`button`,{className:`text-button older`,onClick:Ee,disabled:B,children:`查看更早消息`}),!_e.length&&(0,x.jsx)(j,{title:`从一句话开始`,children:`例如「我下周三去上海出差」。实时节奏会在短暂停顿后开始学习。`}),_e.map(e=>(0,x.jsxs)(`article`,{className:`message ${e.kind===`self_output`?`role`:`user`}`,children:[(0,x.jsxs)(`div`,{className:`message-meta`,children:[(0,x.jsx)(`strong`,{children:e.kind===`self_output`?t.role_name:e.sender_name}),(0,x.jsx)(`time`,{children:S(e.received_at)})]}),(0,x.jsxs)(`div`,{className:`bubble`,children:[e.content,(0,x.jsx)(qe,{media:e.media})]}),(0,x.jsxs)(`div`,{className:`message-state ${[`abandoned`,`refused`].includes(e.learning_state)?`danger-text`:``}`,children:[O(e.learning_state),e.batch_id&&` · 批次 #${e.batch_id}`,e.kind===`self_output`?` · 角色实际输出`:``]})]},e.id))]}),(0,x.jsxs)(`div`,{className:`composer`,children:[(F||o.error)&&(0,x.jsx)(A,{error:!0,children:F||o.error}),te&&(0,x.jsx)(`p`,{className:`receipt`,role:`status`,children:te}),ne&&!D&&(0,x.jsx)(`button`,{className:`secondary`,onClick:we,children:`重试角色回复`}),u&&(0,x.jsxs)(`form`,{className:`inline-form compact`,onSubmit:xe,children:[(0,x.jsxs)(`label`,{children:[`虚拟发言人姓名`,(0,x.jsx)(`input`,{value:p,maxLength:100,required:!0,onChange:e=>m(e.target.value)})]}),(0,x.jsx)(`button`,{className:`secondary`,disabled:!p.trim(),children:`添加发言人`})]}),(0,x.jsxs)(`form`,{onSubmit:Ce,children:[(0,x.jsxs)(`div`,{className:`composer-controls`,children:[(0,x.jsxs)(`label`,{className:`speaker-label`,children:[`发言人`,(0,x.jsx)(`select`,{"aria-label":`发言人`,disabled:D,value:s,onChange:e=>c(e.target.value),children:t.speakers.map(e=>(0,x.jsxs)(`option`,{value:e.id,children:[e.name,t.speakers.filter(t=>t.name===e.name).length>1?` · ${e.id.slice(0,6)}`:``]},e.id))})]}),(0,x.jsx)(`button`,{type:`button`,className:`text-button`,onClick:()=>d(!u),children:`+ 添加`}),(0,x.jsxs)(`label`,{className:`switch`,children:[(0,x.jsx)(`input`,{type:`checkbox`,checked:w,disabled:D,onChange:e=>E(e.target.checked)}),(0,x.jsx)(`span`,{children:`角色回复`})]})]}),(0,x.jsx)(`textarea`,{"aria-label":`消息内容`,placeholder:`说说正在发生的事…`,value:h,onChange:e=>b(e.target.value),onPaste:e=>{let t=Array.from(e.clipboardData.files);t.length&&(e.preventDefault(),H(t))},rows:3,disabled:D}),(0,x.jsx)(Qe,{images:me.images,busy:D,add:H,remove:me.remove}),(0,x.jsxs)(`div`,{className:`composer-bottom`,children:[(0,x.jsx)(`span`,{className:`muted`,children:N||`接收后自动排队学习`}),(0,x.jsxs)(`button`,{className:`primary`,disabled:D||!h.trim()&&!me.images.length||!s,"aria-label":`发送消息`,children:[D?`处理中…`:`发送`,` `,(0,x.jsx)(`span`,{"aria-hidden":!0,children:`↑`})]})]})]})]})]}),(0,x.jsxs)(`aside`,{className:`trial-aside`,"aria-label":`学习与回复上下文`,children:[(0,x.jsxs)(`section`,{className:`panel`,children:[(0,x.jsxs)(`div`,{className:`panel-heading`,children:[(0,x.jsx)(`h2`,{children:`学习动态`}),(0,x.jsx)(`button`,{className:`text-button`,onClick:Te,children:`立即学习`})]}),(0,x.jsxs)(`div`,{className:`queue-summary`,children:[(0,x.jsx)(`strong`,{children:ge?.pending_count??`—`}),(0,x.jsx)(`span`,{children:`条待学习`}),(0,x.jsx)(M,{tone:ge?.current_batch?`purple`:``,children:ge?.current_batch?`批次 #${ge.current_batch.id}`:`等待新消息`})]}),ge?.current_batch&&(0,x.jsxs)(`p`,{className:`batch-state`,children:[k(ge.current_batch),ge.current_batch.next_retry_at&&(0,x.jsxs)(`small`,{children:[`下次尝试:`,S(ge.current_batch.next_retry_at)]})]}),ge?.latest_batch&&(0,x.jsxs)(`div`,{className:`batch-result`,children:[(0,x.jsxs)(`span`,{className:`muted`,children:[`最近结束 · #`,ge.latest_batch.id]}),(0,x.jsx)(`p`,{children:k(ge.latest_batch)}),ge.latest_batch.last_error&&(0,x.jsx)(`small`,{children:ge.latest_batch.last_error})]}),(0,x.jsx)(`p`,{className:`fine-print`,children:`学习成功也可能没有值得记住的内容。`}),(0,x.jsx)(st,{value:ge?.queue_wait})]}),(0,x.jsx)(Wn,{goals:U?.goals,error:o.error}),(0,x.jsxs)(`section`,{className:`panel`,children:[(0,x.jsxs)(`div`,{className:`panel-heading`,children:[(0,x.jsx)(`h2`,{children:`新形成与更新`}),(0,x.jsx)(M,{children:U?.recent_memories.length||0})]}),(0,x.jsx)(`p`,{className:`muted section-help`,children:`本入口最近 12 条,点击查看来源`}),U?.recent_memories.length?U.recent_memories.map(e=>(0,x.jsxs)(`button`,{className:`memory-preview`,onClick:()=>i(e.id),children:[(0,x.jsxs)(`span`,{className:`tiny-label`,children:[e.kind,` · #`,e.id]}),(0,x.jsx)(`span`,{children:e.content}),(0,x.jsxs)(`small`,{children:[S(e.updated_at),` `,(0,x.jsx)(`span`,{"aria-hidden":!0,children:`↗`})]})]},e.id)):(0,x.jsx)(`p`,{className:`quiet`,children:`暂时还没有形成记忆。`})]}),(0,x.jsxs)(`section`,{className:`panel`,children:[(0,x.jsxs)(`div`,{className:`panel-heading`,children:[(0,x.jsx)(`h2`,{children:`这次回复准备`}),(0,x.jsx)(`span`,{className:`muted`,children:re?`${re.memories.length} 条召回`:ae?`当次诊断`:se?`未返回材料`:D?`准备中`:`等待发送`})]}),!re&&!ae?(0,x.jsx)(`p`,{className:`quiet`,children:se||(D?`正在准备回复材料…`:`发送消息后展示召回结果。`)}):(0,x.jsxs)(x.Fragment,{children:[se&&(0,x.jsx)(A,{children:se}),(0,x.jsx)(dt,{value:ae?.judgment||re?.judgment}),ae&&(0,x.jsx)(We,{value:ae,openMemory:i}),re?.memories.length===0&&(0,x.jsx)(`p`,{className:`quiet`,children:`没有额外召回记忆,近期原文仍在对话上下文中。`}),re?.memories.map(e=>(0,x.jsxs)(`div`,{children:[(0,x.jsxs)(`button`,{className:`memory-preview`,onClick:()=>i(e.id),children:[(0,x.jsx)(M,{tone:`purple`,children:e.reason===`person_highlight`?`人物要点`:`与当前内容相关`}),(0,x.jsx)(`span`,{children:e.content})]}),(0,x.jsx)(ft,{value:e.subject_annotations})]},e.id)),(0,x.jsx)(`p`,{className:`fine-print`,children:`被召回不等于被使用,也不代表内容已证实。`}),re?.hints.filter(e=>e.code!==`recall_judgment`).map((e,t)=>(0,x.jsx)(`p`,{className:`fine-print`,children:e.message},t))]})]}),(0,x.jsxs)(`section`,{className:`panel context-panel`,children:[(0,x.jsx)(`h2`,{children:`角色上下文`}),(0,x.jsx)($e,{}),(0,x.jsxs)(`details`,{open:!0,children:[(0,x.jsx)(`summary`,{children:`当前状态`}),(0,x.jsx)(Bn,{})]})]})]})]})}function qn({value:e}){return e?(0,x.jsxs)(x.Fragment,{children:[(0,x.jsxs)(`table`,{className:`image-usage`,children:[(0,x.jsx)(`caption`,{children:`图片理解用量`}),(0,x.jsx)(`thead`,{children:(0,x.jsxs)(`tr`,{children:[(0,x.jsx)(`th`,{scope:`col`,children:`指标`}),(0,x.jsx)(`th`,{scope:`col`,children:`今日`}),(0,x.jsx)(`th`,{scope:`col`,children:`本周`})]})}),(0,x.jsx)(`tbody`,{children:[[`调用次数`,e=>e.calls],[`token(输入+输出)`,e=>e.tokens.toLocaleString()],[`累计耗时`,e=>C(e.duration_ms)],[`失败调用`,e=>e.failures],[`其中被拒绝`,e=>e.refusals],[`超时调用`,e=>e.timeouts],[`未返回 token 用量的调用`,e=>e.calls_without_usage]].map(([t,n])=>(0,x.jsxs)(`tr`,{children:[(0,x.jsx)(`th`,{scope:`row`,children:t}),(0,x.jsx)(`td`,{children:n(e.today)}),(0,x.jsx)(`td`,{children:n(e.week)})]},t))})]}),(0,x.jsx)(`p`,{className:`fine-print`,children:`按角色时区统计,本周自周一开始。含重试、连接测试和恢复探测;未返回的 token 用量不作推算。`})]}):(0,x.jsx)(`p`,{children:`暂时无法读取图片用量。`})}var Jn={manual:`手动维护`,scheduled:`定时维护`,catchup:`启动补跑`},Yn={decay:`衰减与状态转换`,expiry:`到期删除`,dependency:`依据失效扣减`,messages:`消息清理`,retry:`批次重试`,consolidation:`模型整理`,persona:`persona 更新`,goals:`目标依据复核`},Xn={decayed:`衰减`,forgotten:`遗忘`,restored:`恢复`,deleted:`删除`,dependencies_weakened:`依据扣减`,messages_deleted:`清理消息`,batches_retried:`重试批次`,questions_expired:`询问到期结束`,checked:`检查项目`,skipped:`跳过`,failed:`失败`,merged:`合并`,conflicts:`待核对建议(矛盾或合并)`,dependencies_reviewed:`依赖复核建议`,persona_published:`persona 已发布`,persona_pending:`persona 待确认`,person_summary:`人物摘要处理`,goals_reviewed:`目标依据标注变化`,goals_auto_completed:`目标自动完成`,goals_auto_abandoned:`目标自动放弃`,goal_review_kept:`目标复核保持未结束`,goal_duplicates_merged:`重复目标合并`,goal_duplicates_separated:`撤销可能重复`,goal_duplicates_kept:`保留可能重复`,model_calls:`模型调用`},Zn=[`goals_auto_completed`,`goals_auto_abandoned`,`goal_duplicates_merged`,`goal_duplicates_separated`],Qn={pinned:`已置顶`,revision_conflict:`修订冲突`,forgotten:`已遗忘`,deleted:`已删除`,no_longer_due:`尚未到期`,already_applied:`已经扣减`,dependency_removed:`依据关系已移除`,no_longer_eligible:`不再符合清理条件`,referenced:`仍有引用`,batch_state_changed:`批次状态已变化`,already_retried:`已自动重试过`,batch_date_changed:`批次日期已变化`,target_messages_cleared:`目标消息已清理`};function $n({summary:e}){return(0,x.jsx)(`dl`,{className:`maintenance-counts`,children:Object.entries(e).filter(([e])=>![`model_step_budget`,`backlog_trimmed`].includes(e)).map(([e,t])=>(0,x.jsxs)(`div`,{children:[(0,x.jsx)(`dt`,{children:Xn[e]||e}),(0,x.jsx)(`dd`,{children:t.count})]},e))})}function er({run:e}){return(0,x.jsxs)(`div`,{className:`lifecycle-stack`,children:[(0,x.jsxs)(`div`,{className:`actions`,children:[(0,x.jsx)(M,{tone:e.state===`completed`?`green`:`purple`,children:e.state===`completed`?`已完成`:`运行中`}),(0,x.jsx)(`span`,{children:Jn[e.trigger]||e.trigger}),(0,x.jsxs)(`span`,{className:`muted`,children:[`#`,e.id]})]}),(0,x.jsxs)(`p`,{children:[`开始:`,w(e.created_at),` · 完成:`,e.finished_at?w(e.finished_at):`尚未完成`]})]})}function tr({openMemory:e,initialRun:t}){let n=y(`/maintenance?limit=1&offset=0`,5e3),[r,i]=(0,l.useState)(!1),[a,o]=(0,l.useState)(0),s=y(r?`/maintenance?limit=10&offset=${a}`:null,5e3),[c,u]=(0,l.useState)(t||null),[d,f]=(0,l.useState)(!1),[p,m]=(0,l.useState)(!1),[h,b]=(0,l.useState)(``),S=n.data?.items[0];(0,l.useEffect)(()=>{let e=()=>{let e=location.hash;if(e.split(`?`)[0]!==`#/status`)return;let t=Number(new URLSearchParams(e.split(`?`)[1]||``).get(`run`));Number.isSafeInteger(t)&&t>0&&u(t)};return addEventListener(`hashchange`,e),()=>removeEventListener(`hashchange`,e)},[]);async function C(){if(!p){m(!0),b(``);try{let e=await g(`/maintenance`,_(`POST`,{}));f(!1),u(e.run_id),n.refresh(),s.refresh()}catch(e){b(v(e))}finally{m(!1)}}}return(0,x.jsxs)(`section`,{className:`panel maintenance-panel`,children:[(0,x.jsxs)(`div`,{className:`panel-heading`,children:[(0,x.jsx)(`h2`,{children:`梦境整理与记忆维护`}),(0,x.jsx)(`button`,{className:`secondary`,disabled:p,onClick:()=>f(!0),children:`手动维护`})]}),(h||n.error||s.error)&&(0,x.jsxs)(A,{error:!0,children:[h||n.error||s.error,(0,x.jsx)(`button`,{className:`text-button`,onClick:()=>{n.refresh(),s.refresh()},children:`刷新维护记录`})]}),d&&(0,x.jsxs)(`div`,{className:`delete-confirm`,children:[(0,x.jsx)(`p`,{children:`手动维护也计一次衰减,并执行遗忘、到期删除、消息清理和批次重试。还会按本次设置调用模型进行整理与 persona 与人物摘要更新,复核目标依据;目标自动复核和可能重复补判开启时也会执行。已有未完成的维护时继续该次运行。`}),(0,x.jsxs)(`div`,{className:`actions`,children:[(0,x.jsx)(`button`,{className:`primary`,disabled:p,onClick:()=>void C(),children:p?`正在请求…`:`确认运行维护`}),(0,x.jsx)(`button`,{className:`secondary`,disabled:p,onClick:()=>f(!1),children:`取消维护`})]})]}),!n.data&&!n.error&&(0,x.jsx)(`p`,{role:`status`,children:`正在读取维护记录…`}),n.data&&!S&&(0,x.jsx)(j,{title:`尚无维护记录`}),S&&(0,x.jsxs)(x.Fragment,{children:[(0,x.jsx)(`h3`,{className:`maintenance-subheading`,children:`最近一次维护`}),(0,x.jsx)(er,{run:S}),Object.keys(S.summary).length?(0,x.jsx)($n,{summary:S.summary}):(0,x.jsx)(`p`,{className:`quiet`,children:`维护进行中,可在报告中查看当前数量。`}),(0,x.jsx)(`button`,{className:`text-button`,onClick:()=>u(S.id),children:`查看最近维护报告`})]}),(0,x.jsx)(`p`,{className:`fine-print`,children:`维护不中断接收和学习。报告逐项保留实际变化、模型建议和失败;检查及跳过数量按阶段和原因汇总。`}),(0,x.jsx)(`button`,{className:`text-button`,"aria-expanded":r,onClick:()=>i(e=>!e),children:`历史维护`}),r&&(0,x.jsxs)(`div`,{className:`lifecycle-stack`,children:[!s.data&&!s.error&&(0,x.jsx)(`p`,{role:`status`,children:`正在读取历史维护…`}),s.data?.items.length===0&&(0,x.jsx)(`p`,{className:`quiet`,children:`没有历史维护。`}),s.data?.items.map(e=>(0,x.jsxs)(`div`,{className:`maintenance-history-row`,children:[(0,x.jsxs)(`span`,{children:[`#`,e.id,` · `,w(e.created_at),` ·`,` `,Jn[e.trigger]||e.trigger,` ·`,` `,e.state===`completed`?`已完成`:`运行中`]}),(0,x.jsxs)(`button`,{className:`text-button`,onClick:()=>u(e.id),children:[`查看报告 #`,e.id]})]},e.id)),s.data&&(0,x.jsx)(T,{total:s.data.total,limit:10,offset:a,change:o})]}),c!==null&&(0,x.jsx)(nr,{id:c,close:()=>{u(null),t&&(location.hash=`#/status`)},openMemory:t=>{u(null),location.hash.startsWith(`#/status?`)&&window.history.replaceState(null,``,`#/status`),e(t)}},c)]})}function nr({id:e,close:t,openMemory:n}){let[r,i]=(0,l.useState)(3e3),a=y(`/maintenance/${e}`,r),o=a.data;(0,l.useEffect)(()=>{o?.state===`completed`&&i(0)},[o?.state]);let s=o?[[`变化的记忆`,o.items.filter(e=>e.memory_id!=null&&e.outcome!==`failed`&&e.phase!==`consolidation`)],[`清理的消息`,o.items.filter(e=>e.outcome===`messages_deleted`)],[`重试的批次`,o.items.filter(e=>e.outcome===`batches_retried`)],[`失败项`,o.items.filter(e=>e.outcome===`failed`)],[`目标复核保持不变`,o.items.filter(e=>[`goal_review_kept`,`goal_duplicates_kept`].includes(e.outcome))]]:[];return(0,x.jsxs)(ee,{title:`维护报告 #${e}`,onClose:t,children:[a.error&&(0,x.jsxs)(A,{error:!0,children:[a.error,(0,x.jsx)(`button`,{className:`text-button`,onClick:a.refresh,children:`重新读取报告`})]}),!o&&!a.error&&(0,x.jsx)(`p`,{role:`status`,children:`正在读取维护报告…`}),o&&(0,x.jsxs)(`div`,{className:`lifecycle-stack`,children:[(0,x.jsx)(er,{run:o}),(0,x.jsxs)(`p`,{className:`muted`,children:[`角色时区:`,o.timezone,` · 当前阶段:`,o.state===`completed`?`已结束`:Object.values(Yn)[o.phase]||`生成报告`]}),(0,x.jsx)($n,{summary:o.summary}),(0,x.jsxs)(`details`,{children:[(0,x.jsx)(`summary`,{children:`检查阶段与跳过原因`}),Object.entries(o.summary.checked?.by_phase||{}).map(([e,t])=>(0,x.jsxs)(`p`,{children:[Yn[e]||e,`:`,t]},e)),Object.entries(o.summary.skipped?.reasons||{}).map(([e,t])=>(0,x.jsxs)(`p`,{children:[Qn[e]||pe(e),`:`,t]},e))]}),(0,x.jsx)(sn,{data:o,openMemory:n,refresh:a.refresh}),(0,x.jsx)(ir,{data:o,items:o.items.filter(e=>Zn.includes(e.outcome)),refresh:a.refresh}),s.map(([t,r])=>(0,x.jsx)(rr,{name:t,items:r,openMemory:n},`${e}:${t}`))]})]})}function rr({name:e,items:t,openMemory:n}){let[r,i]=(0,l.useState)(0);return(0,x.jsxs)(`section`,{className:`detail-section`,"aria-label":e,children:[(0,x.jsxs)(`h3`,{children:[e,` `,(0,x.jsx)(M,{children:t.length})]}),!t.length&&(0,x.jsx)(`p`,{className:`quiet`,children:`暂无记录。`}),t.slice(r,r+30).map(e=>(0,x.jsxs)(`article`,{className:`maintenance-item`,children:[e.memory_id==null?(0,x.jsxs)(`strong`,{children:[e.phase===`messages`?`消息`:e.phase===`persona`?`persona 任务`:e.phase===`goals`?`目标`:`批次`,` `,`#`,e.object_id]}):(0,x.jsxs)(`button`,{className:`text-button`,onClick:()=>n(e.memory_id),children:[`记忆 #`,e.memory_id]}),(0,x.jsxs)(`p`,{children:[Yn[e.phase]||e.phase,` ·`,` `,Xn[e.outcome]||e.outcome]}),typeof e.details.before==`number`&&(0,x.jsxs)(`p`,{children:[`保留强度:`,e.details.before,` → `,e.details.after]}),e.details.transition&&(0,x.jsxs)(`p`,{children:[`状态变为:`,E(e.details.transition)]}),e.details.source_memory_id!==void 0&&(0,x.jsxs)(`p`,{children:[`失效依据:记忆 #`,e.details.source_memory_id]}),e.reason&&(0,x.jsx)(`p`,{className:`danger-text`,children:e.phase===`goals`&&Tt[e.reason]||Qn[e.reason]||pe(e.reason)}),(0,x.jsx)(`small`,{children:w(e.created_at)})]},`${e.phase}:${e.item_key}`)),(0,x.jsx)(T,{total:t.length,offset:r,change:i})]})}function ir({data:e,items:t,refresh:n}){let[r,i]=(0,l.useState)(null),[a,o]=(0,l.useState)(``),[s,c]=(0,l.useState)([]),[u,d]=(0,l.useState)([]),p=[[`目标自动复核`,e.goal_auto_review],[`可能重复补判`,e.goal_duplicate_review]];if(!p.some(([,e])=>e?.enabled)&&!t.length)return null;async function m(e,t){if(r===null){i(e),o(``);try{await g(`/goals/${e}/reopen`,_(`POST`,{expected_revision:t})),c(t=>[...t,e]),n()}catch(t){o(t instanceof f&&t.status===409?`目标 #${e} 已变化,请到目标页核对后处理。`:v(t))}finally{i(null)}}}async function h(e,t){if(r===null){i(e),o(``);try{await g(`/goal-duplicate-judgments/${e}/undo`,_(`POST`,{expected_revision:t})),d(t=>[...t,e]),n()}catch(e){o(e instanceof f&&e.status===409?`保留目标在合并后已变化,不能撤销;请到目标页核对。`:v(e))}finally{i(null)}}}return(0,x.jsxs)(`section`,{className:`detail-section`,"aria-label":`目标自动复核`,children:[(0,x.jsxs)(`h3`,{children:[`目标自动复核 `,(0,x.jsx)(M,{children:t.length})]}),p.map(([e,t])=>(0,x.jsxs)(`p`,{className:`muted`,children:[e,`:`,t?.enabled?[[`calls`,`模型调用`],[`closed`,`关闭`],[`kept_open`,`保持未结束`],[`merged`,`合并`],[`separated`,`撤销标记`],[`kept`,`保留标记`],[`no_candidates`,`无候选消息`],[`already_judged`,`未变化`],[`budget_exhausted`,`预算用尽`],[`merged_this_run`,`本轮刚合并`],[`failed`,`失败`]].filter(([e])=>typeof t[e]==`number`).map(([e,n])=>`${n} ${t[e]}`).join(` · `)||`无可处理目标`:`本次未开启`,t?.stopped&&` · 提前停止:${Et(t.stopped)}`]},e)),a&&(0,x.jsx)(A,{error:!0,children:a}),t.map(e=>{let t=e.details.kind===`duplicate`?e.details.target_id??e.object_id:e.object_id,n=e.outcome.startsWith(`goals_auto_`);return(0,x.jsxs)(`article`,{className:`maintenance-item`,children:[(0,x.jsx)(`strong`,{children:e.details.kind===`duplicate`?`目标 #${(e.details.goal_ids||[]).join(` 与 #`)}`:`目标 #${e.object_id}`}),(0,x.jsxs)(`p`,{children:[Xn[e.outcome]||e.outcome,` · `,Et(e.reason)]}),n&&(0,x.jsxs)(`p`,{children:[`依据消息:`,(e.details.evidence||[]).map(e=>`#${e}`).join(`、`)]}),(0,x.jsxs)(`div`,{className:`actions`,children:[(0,x.jsxs)(`a`,{className:`text-button`,href:`#/state?tab=goals&id=${t}`,children:[`查看目标 #`,t]}),e.outcome===`goal_duplicates_merged`&&e.details.judgment_id!=null&&e.details.target_revision_after!=null&&(u.includes(e.details.judgment_id)?(0,x.jsx)(`span`,{children:`已撤销合并`}):(0,x.jsxs)(`button`,{disabled:r!==null,onClick:()=>void h(e.details.judgment_id,e.details.target_revision_after),children:[`撤销合并 #`,e.details.judgment_id]})),n&&e.details.revision_after!=null&&(s.includes(e.object_id)?(0,x.jsx)(`span`,{children:`已重开`}):(0,x.jsxs)(`button`,{disabled:r!==null,onClick:()=>void m(e.object_id,e.details.revision_after),children:[`重开目标 #`,e.object_id]}))]}),(0,x.jsx)(`small`,{children:w(e.created_at)})]},e.item_key)})]})}var ar={learning:`学习`,learning_repair:`学习 JSON 修正`,trial_reply:`试用回复`,trial_reply_repair:`回复 JSON 修正`,embedding:`向量生成`,image_understanding:`图片理解`,retrieval_query:`召回查询`,health_probe:`健康探测`,recall_judge:`召回判断`,goal_dedup_judge:`目标去重判断`,consolidation_merge:`整理合并判断`,consolidation_merge_repair:`整理合并 JSON 修正`,consolidation_conflict:`整理矛盾建议`,consolidation_conflict_repair:`整理矛盾 JSON 修正`,consolidation_dependency:`整理依赖复核`,consolidation_dependency_repair:`整理依赖 JSON 修正`,persona_generate:`persona 生成`,persona_check:`persona 检查`,connection_check:`连接测试`};function or({data:e,openMemory:t,initialQuery:n=``}){let r=y(`/catalog`),i=y(`/settings`,5e3);if(!e)return(0,x.jsx)(j,{title:`正在读取运行状态`});let a=Number(new URLSearchParams(n).get(`run`)),o=e.learning_latency_24h,s=e.models.find(e=>e.purpose===`image_understanding`),c=e.models.filter(e=>e.result_category!==`success`),l=new Map(r.data?.entries.map(e=>[e.id,e.name]));return(0,x.jsxs)(x.Fragment,{children:[(0,x.jsxs)(`div`,{className:`page-heading`,children:[(0,x.jsxs)(`div`,{children:[(0,x.jsx)(`p`,{className:`eyebrow`,children:`每一步,都看得见`}),(0,x.jsx)(`h1`,{children:`运行状态`}),(0,x.jsx)(`p`,{children:`服务、模型和学习队列的当前情况。`})]}),(0,x.jsx)(M,{tone:e.scheduler.running?`green`:`danger`,children:e.scheduler.running?`调度运行中`:`调度未运行`})]}),(0,x.jsxs)(`div`,{className:`stat-grid`,children:[(0,x.jsxs)(`section`,{className:`panel metric`,children:[(0,x.jsx)(`span`,{children:`待学习消息`}),(0,x.jsx)(`strong`,{children:e.entries.reduce((e,t)=>e+t.pending_count,0)}),(0,x.jsxs)(`small`,{children:[e.scheduler.max_concurrent,` 个学习批次可同时运行`]})]}),(0,x.jsxs)(`section`,{className:`panel metric`,children:[(0,x.jsx)(`span`,{children:`今日 token 用量`}),(0,x.jsx)(`strong`,{children:e.usage.today.tokens.toLocaleString()}),(0,x.jsxs)(`small`,{children:[e.usage.today.calls,` 次请求 ·`,` `,e.usage.today.reasoning_tokens.toLocaleString(),` 推理 token(包含在总量内)`]})]}),(0,x.jsxs)(`section`,{className:`panel metric`,children:[(0,x.jsx)(`span`,{children:`近 24 小时学习 P95`}),(0,x.jsx)(`strong`,{children:C(o.p95_ms)}),(0,x.jsxs)(`small`,{children:[o.count,` 次学习调用`]})]}),(0,x.jsxs)(`section`,{className:`panel metric`,children:[(0,x.jsx)(`span`,{children:`近 24 小时学习超时`}),(0,x.jsx)(`strong`,{children:o.timeouts}),(0,x.jsxs)(`small`,{children:[`共享学习总预算 `,(0,x.jsxs)(`b`,{children:[e.timeouts_seconds.learning,` 秒`]})]})]})]}),(0,x.jsx)(tr,{openMemory:t,initialRun:Number.isSafeInteger(a)&&a>0?a:void 0}),(0,x.jsxs)(`div`,{className:`status-grid`,children:[(0,x.jsxs)(`section`,{className:`panel`,children:[(0,x.jsxs)(`div`,{className:`panel-heading`,children:[(0,x.jsx)(`h2`,{children:`模型用途`}),(0,x.jsx)(`span`,{className:`muted`,children:`实时健康状态`})]}),Object.entries(e.model_health).map(([t,n])=>(0,x.jsxs)(`div`,{className:`model-row`,children:[(0,x.jsxs)(`div`,{className:`model-title`,children:[(0,x.jsx)(`strong`,{children:Ee[t]||t}),(0,x.jsx)(M,{tone:n.state===`normal`?`green`:`danger`,children:D(n.state)})]}),(0,x.jsx)(`p`,{className:`muted`,children:t===`chat`?`用于学习与角色回复`:t===`recall_judge`?`判断相关候选能否回答当前请求`:t===`goal_dedup_judge`?`跨入口判断目标是否为同一件事`:t===`image_understanding`?`为没有宿主说明的图片补充理解;失败时使用占位继续学习`:`用于记忆向量与语义检索`}),t===`recall_judge`&&(0,x.jsxs)(x.Fragment,{children:[e.timeouts_seconds.recall_judge!=null&&(0,x.jsxs)(`p`,{children:[`总预算 `,e.timeouts_seconds.recall_judge,` 秒(含排队)`]}),i.error&&(0,x.jsxs)(`p`,{className:`muted`,children:[`判断开关状态暂时无法读取。`,(0,x.jsx)(`button`,{onClick:i.refresh,children:`重新读取`})]}),i.data?.recall_judge?.enabled===!1?(0,x.jsx)(`p`,{children:`召回判断已关闭,保留基础召回。`}):n.state!==`normal`&&(0,x.jsx)(`p`,{children:`召回判断暂停或退避时降级,保留仍有效的原召回。`}),n.retry_at&&(0,x.jsxs)(`p`,{children:[`退避截止:`,S(n.retry_at)]}),(0,x.jsx)(`a`,{href:`#/settings`,children:`管理召回判断设置`})]}),t===`goal_dedup_judge`&&(0,x.jsxs)(x.Fragment,{children:[i.error&&(0,x.jsxs)(`p`,{className:`muted`,children:[`判断设置暂时无法读取。`,(0,x.jsx)(`button`,{onClick:i.refresh,children:`重新读取`})]}),i.data?.goal_dedup_judge&&(0,x.jsxs)(`p`,{children:[`判断预算 `,i.data.goal_dedup_judge.budget_seconds,` `,`秒(含排队) · 并发`,` `,i.data.goal_dedup_judge.concurrency,` · 排队上限`,` `,i.data.goal_dedup_judge.queue_limit]}),i.data?.goal_dedup_judge?.enabled===!1?(0,x.jsx)(`p`,{children:`目标去重判断已关闭,使用确定性规则。`}):(0,x.jsx)(`p`,{children:`模型暂停、退避或判断失败时先保留目标,等待复核。管理员创建的目标不会自动合并。`}),n.retry_at&&(0,x.jsxs)(`p`,{children:[`退避截止:`,S(n.retry_at)]}),(0,x.jsx)(`a`,{href:`#/settings`,children:`管理目标去重判断设置`})]}),t===`image_understanding`&&(0,x.jsxs)(x.Fragment,{children:[(0,x.jsxs)(`p`,{children:[n.inherited?`沿用对话模型`:`使用独立图片理解配置`,` · 健康状态按图片理解用途独立记录。`]}),n.timeout_seconds!=null&&(0,x.jsxs)(`p`,{children:[`整批图片理解预算 `,n.timeout_seconds,` 秒(含排队与重试)`]}),n.retry_at&&(0,x.jsxs)(`p`,{children:[`退避截止:`,S(n.retry_at)]}),(0,x.jsx)(`a`,{href:`#/settings`,children:`管理图片理解设置`})]}),n.last_error&&(0,x.jsxs)(`p`,{className:`error-summary`,children:[`最近错误:`,n.last_error]}),n.next_probe_at&&(0,x.jsxs)(`small`,{children:[`下次探测:`,S(n.next_probe_at)]})]},t)),!e.model_health.image_understanding&&i.data?.models?.image_understanding?.enabled===!1&&(0,x.jsx)(`p`,{children:`图片理解未启用,没有宿主说明的图片以“未理解”占位继续学习。`}),(e.model_health.image_understanding||s||e.usage.by_purpose?.image_understanding)&&(0,x.jsxs)(`div`,{className:`model-row`,children:[(0,x.jsx)(`h3`,{children:`图片理解调用与用量`}),(0,x.jsx)(qn,{value:e.usage.by_purpose?.image_understanding}),s&&(0,x.jsxs)(x.Fragment,{children:[(0,x.jsxs)(`p`,{children:[`最近调用:`,s.model,` ·`,` `,C(s.duration_ms),` ·`,` `,s.result_category===`success`?`成功`:`未成功`]}),(0,x.jsx)(`p`,{className:`muted`,children:S(s.created_at)})]})]})]}),(0,x.jsxs)(`section`,{className:`panel`,children:[(0,x.jsx)(`h2`,{children:`用量与学习`}),(0,x.jsxs)(`dl`,{className:`status-facts`,children:[(0,x.jsxs)(`div`,{children:[(0,x.jsx)(`dt`,{children:`每日 token 上限`}),(0,x.jsx)(`dd`,{children:e.budget.limit==null?`不限`:e.budget.limit.toLocaleString()})]}),(0,x.jsxs)(`div`,{children:[(0,x.jsx)(`dt`,{children:`未报告 token 的调用`}),(0,x.jsxs)(`dd`,{children:[e.usage.today.calls_without_usage,` 次`]})]}),(0,x.jsxs)(`div`,{children:[(0,x.jsx)(`dt`,{children:`学习耗时 P50 / 最大`}),(0,x.jsxs)(`dd`,{children:[C(o.p50_ms),` / `,C(o.max_ms)]})]}),(0,x.jsxs)(`div`,{children:[(0,x.jsx)(`dt`,{children:`缺少向量的记忆`}),(0,x.jsxs)(`dd`,{children:[e.missing_vectors,` 条`]})]}),(0,x.jsxs)(`div`,{children:[(0,x.jsx)(`dt`,{children:`记忆缺口`}),(0,x.jsxs)(`dd`,{children:[e.memory_gap_count,` 段`]})]})]}),(0,x.jsx)(`p`,{className:`fine-print`,children:`今日按角色时区统计。服务商未报告的用量和在途调用不计入已知总量;未配置价格,暂不估算费用。`})]})]}),(0,x.jsxs)(`section`,{className:`panel`,children:[(0,x.jsxs)(`div`,{className:`panel-heading`,children:[(0,x.jsx)(`h2`,{children:`入口队列`}),(0,x.jsx)(`span`,{className:`muted`,children:`接收、学习和形成记忆分别记录`})]}),e.entries.length?(0,x.jsx)(`div`,{className:`table-scroll`,children:(0,x.jsxs)(`table`,{children:[(0,x.jsx)(`thead`,{children:(0,x.jsxs)(`tr`,{children:[(0,x.jsx)(`th`,{children:`入口`}),(0,x.jsx)(`th`,{children:`待学习`}),(0,x.jsx)(`th`,{children:`当前批次`}),(0,x.jsx)(`th`,{children:`过滤与等待`}),(0,x.jsx)(`th`,{children:`最近结束的批次`}),(0,x.jsx)(`th`,{children:`未记住的消息`})]})}),(0,x.jsx)(`tbody`,{children:e.entries.map(e=>(0,x.jsxs)(`tr`,{children:[(0,x.jsx)(`td`,{children:l.get(e.entry_id)||e.entry_id}),(0,x.jsx)(`td`,{children:e.pending_count}),(0,x.jsx)(`td`,{children:k(e.current_batch)}),(0,x.jsx)(`td`,{children:(0,x.jsx)(st,{value:e.queue_wait})}),(0,x.jsx)(`td`,{children:k(e.latest_batch)}),(0,x.jsx)(`td`,{children:e.gap_message_count||0})]},e.entry_id))})]})}):(0,x.jsx)(`p`,{className:`quiet`,children:`暂无入口。`})]}),(0,x.jsxs)(`div`,{className:`status-grid`,children:[(0,x.jsxs)(`section`,{className:`panel`,children:[(0,x.jsx)(`h2`,{children:`最近学习调用`}),e.learning_calls_24h.length?(0,x.jsx)(`div`,{className:`call-list`,children:e.learning_calls_24h.slice(0,12).map((e,t)=>(0,x.jsxs)(`div`,{className:`call`,children:[(0,x.jsxs)(`span`,{children:[`批次 #`,e.batch_id,` · `,ar[e.purpose]||e.purpose]}),(0,x.jsx)(`strong`,{children:C(e.duration_ms)}),e.timed_out?(0,x.jsx)(M,{tone:`danger`,children:`超时`}):e.error_summary?(0,x.jsx)(M,{tone:`danger`,children:`失败`}):(0,x.jsx)(M,{tone:`green`,children:`调用完成`}),e.error_summary&&(0,x.jsx)(`small`,{children:e.error_summary})]},t))}):(0,x.jsx)(`p`,{className:`quiet`,children:`近 24 小时没有学习调用。`}),(0,x.jsx)(`p`,{className:`fine-print`,children:`模型调用完成不等于批次成功;JSON 校验和记忆写入结果请看批次。`})]}),(0,x.jsxs)(`section`,{className:`panel`,children:[(0,x.jsx)(`h2`,{children:`错误摘要`}),(0,x.jsx)(`p`,{className:`muted section-help`,children:`来自现有状态接口的最近用途错误与调度错误`}),e.scheduler.last_error&&(0,x.jsx)(A,{error:!0,children:e.scheduler.last_error}),c.map(e=>(0,x.jsxs)(`div`,{className:`error-entry`,children:[(0,x.jsxs)(`strong`,{children:[ar[e.purpose]||e.purpose,` · `,e.model]}),(0,x.jsx)(`p`,{children:e.error_summary||e.result_category}),(0,x.jsx)(`small`,{children:S(e.created_at)})]},e.purpose)),!c.length&&!e.scheduler.last_error&&(0,x.jsx)(`p`,{className:`quiet`,children:`最近的用途调用没有错误。`})]})]})]})}var sr=e=>e.execution_state===`split`||e.state===`split`,cr=e=>({full:`完整`,target_only:`仅目标`})[e||``]||`未记录`;function lr(e,t){return sr(e)?[`waiting`,`running`].includes(e.state)?`拆小重学中`:e.state===`succeeded`?`拆小重学已完成`:e.state===`split`?`历史请求被拒绝 · 已拆分`:`拆小重学结束 · 仍有缺口`:e.parent_batch_id&&[`waiting`,`running`].includes(e.state)?`拆小重学中`:e.state===`refused`&&(t===1||t===void 0&&e.parent_batch_id)?`该单消息的学习请求被拒绝`:e.relearning?`重新学习中`:k(e)}function ur(e){return new Set([...e.attempts.flatMap(e=>e.calls.map(e=>e.id)),...e.unassigned_calls.map(e=>e.id)]).size}function dr({batch:e}){let t=e.split;if(!t)return null;let n=t.waiting_messages+t.running_messages,r=t.refused_messages+t.abandoned_messages,i=n+r+t.succeeded_messages,a=e.state===`succeeded`&&n===0&&r===0&&t.gap_count===0;return(0,x.jsxs)(`div`,{className:`split-progress`,children:[(0,x.jsxs)(`div`,{className:`split-metrics`,"aria-label":`子树消息进度`,children:[(0,x.jsxs)(`span`,{children:[e.parent_batch_id?`本子树目标`:`原目标`,` `,i,` 条`]}),(0,x.jsxs)(`span`,{children:[`已成功 `,t.succeeded_messages,` 条`]}),(0,x.jsxs)(`span`,{children:[`仍在补学 `,n,` 条`]}),(0,x.jsxs)(`span`,{children:[`最终被拒绝 `,t.refused_messages,` 条`]}),(0,x.jsxs)(`span`,{children:[`最终失败 `,r,` 条`]}),(0,x.jsxs)(`span`,{children:[`剩余缺口 `,t.gap_count,` 处`]})]}),(0,x.jsxs)(`p`,{children:[`批次树节点 `,t.node_count,` 个`]}),(0,x.jsx)(`p`,{className:`muted`,children:`消息结果仅统计末端子批次;最终失败包含拒绝与尝试用尽。父批次的历史拒绝单独保留。`}),a&&(0,x.jsx)(`p`,{className:`split-complete`,children:`全部成功,无缺口`}),!a&&(0,x.jsx)(`p`,{children:`缺口在对应子批次成功后消失;每处缺口可能包含多条消息。`})]})}function fr({batch:e,onOpen:t}){return e.parent_batch_id?(0,x.jsxs)(`div`,{className:`batch-links`,children:[e.root_batch_id&&(0,x.jsxs)(`button`,{className:`text-button`,onClick:()=>t(e.root_batch_id),"aria-label":`查看原始批次 #${e.root_batch_id}`,children:[`原始批次 #`,e.root_batch_id,` →`]}),e.parent_batch_id!==e.root_batch_id&&(0,x.jsxs)(`button`,{className:`text-button`,onClick:()=>t(e.parent_batch_id),"aria-label":`查看父批次 #${e.parent_batch_id}`,children:[`父批次 #`,e.parent_batch_id,` →`]}),(0,x.jsxs)(`span`,{children:[`路径 `,e.split_path]})]}):null}function pr(e){return e.count?`第 ${e.start+1}${e.count>1?`–${e.end}`:``} 条 · 共 ${e.count} 条`:`没有目标消息`}function mr({id:e}){let t=y(`/batches/${e}/tree?limit=1&offset=0`,3e3),n=t.data?.items.find(t=>t.id===e);return(0,x.jsxs)(x.Fragment,{children:[t.error&&(0,x.jsxs)(A,{error:!0,children:[t.error,(0,x.jsx)(`button`,{onClick:t.refresh,children:`重试读取目标范围`})]}),n&&(0,x.jsxs)(`p`,{children:[`原始目标范围:`,pr(n.target_range)]})]})}function hr({batch:e}){let t=sr(e);return(0,x.jsxs)(`div`,{className:`batch-node-facts`,children:[(0,x.jsxs)(`p`,{children:[`上下文模式:`,cr(e.context_mode)]}),t?(0,x.jsxs)(`p`,{children:[`拆分前已计 `,e.attempt_count??0,` 次尝试;执行已交给子批次。`]}):(0,x.jsxs)(`p`,{children:[`当前额度:已用 `,e.attempt_count??0,` / 4,剩余`,` `,e.attempts_remaining??Math.max(0,4-(e.attempt_count??0)),` `,`次`]}),e.state===`waiting`&&!t&&(0,x.jsx)(`p`,{children:`排队原因未提供;请结合入口和运行状态查看。`}),!t&&(0,x.jsxs)(`p`,{children:[`下次重试:`,e.next_retry_at?S(e.next_retry_at):`无定时时间`]})]})}function gr({id:e}){let t=y(`/batches/${e}`,3e3);return(0,x.jsxs)(x.Fragment,{children:[t.error&&(0,x.jsxs)(A,{error:!0,children:[t.error,(0,x.jsx)(`button`,{onClick:t.refresh,children:`重试读取历史`})]}),t.data?(0,x.jsxs)(x.Fragment,{children:[(0,x.jsxs)(`p`,{children:[`历史尝试编号:`,t.data.attempts.length?t.data.attempts.map(e=>e.number).join(`、`):`暂无已完成尝试`]}),(0,x.jsxs)(`p`,{children:[`本节点实际模型调用 `,ur(t.data),` 次`]}),(0,x.jsx)(`p`,{className:`muted`,children:`含学习与 JSON 修正请求及未归入尝试的调用;不等于本轮已用额度。`})]}):(0,x.jsx)(`p`,{children:`正在读取历史…`})]})}function _r({id:e}){let[t,n]=(0,l.useState)(!1);return(0,x.jsxs)(`details`,{className:`node-history`,onToggle:e=>n(e.currentTarget.open),children:[(0,x.jsxs)(`summary`,{children:[`历史尝试与调用 · #`,e]}),t&&(0,x.jsx)(gr,{id:e})]})}function vr({id:e,onOpen:t}){let[n,r]=(0,l.useState)(!1),[i,a]=(0,l.useState)(0),[o,s]=(0,l.useState)([]),c=y(n?`/batches/${e}/tree?limit=100&offset=${i}`:null,3e3),u=c.data;return(0,x.jsxs)(`div`,{className:`split-tree`,children:[(0,x.jsx)(`button`,{className:`secondary`,"aria-expanded":n,"aria-controls":`split-tree-${e}`,"aria-label":`${n?`收起`:`展开`}拆分树 #${e}`,onClick:()=>r(!n),children:n?`收起拆分树`:`展开拆分树`}),n&&(0,x.jsxs)(`section`,{id:`split-tree-${e}`,"aria-label":`拆分树 #${e}`,children:[(0,x.jsx)(`p`,{className:`muted`,children:`范围按原始批次的目标消息顺序,从第 1 条开始;路径中 0 为左支、1 为右支。节点可展开历史尝试,或进入详情查看消息与调用。`}),c.error&&(0,x.jsxs)(A,{error:!0,children:[c.error,(0,x.jsx)(`button`,{onClick:c.refresh,children:`重试读取拆分树`})]}),u?(0,x.jsxs)(x.Fragment,{children:[(0,x.jsxs)(`p`,{children:[`显示节点 `,u.total?u.offset+1:0,`–`,u.offset+u.items.length,` / `,u.total]}),(0,x.jsx)(`ol`,{className:`batch-tree-nodes`,children:u.items.filter(e=>!o.some(t=>e.split_path!==t&&e.split_path?.startsWith(t))).map(n=>{let r=sr(n),i=n.split_path||``,a=n.target_range,c=o.includes(i);return(0,x.jsxs)(`li`,{className:`batch-tree-node`,style:{"--tree-depth":Math.min(i.length,3)},children:[(0,x.jsxs)(`div`,{className:`section-title`,children:[(0,x.jsxs)(`strong`,{children:[`#`,n.id,` · `,r?`拆分父批次`:`子批次`]}),(0,x.jsx)(M,{children:lr(n,a.count)})]}),(0,x.jsx)(`p`,{children:pr(a)}),(0,x.jsxs)(`p`,{children:[`路径 `,i||`根`]}),n.parent_batch_id&&(0,x.jsxs)(`button`,{className:`text-button`,"aria-label":`查看父批次 #${n.parent_batch_id}`,onClick:()=>t(n.parent_batch_id),children:[`父批次 #`,n.parent_batch_id,` →`]}),(0,x.jsx)(hr,{batch:n}),n.gap&&(0,x.jsxs)(`p`,{children:[`仍有记忆缺口`,[`waiting`,`running`].includes(n.state)?` · 拆小重学中`:``]}),n.relearn_blocked_reason&&!r&&(0,x.jsx)(`p`,{children:n.relearn_blocked_reason}),(0,x.jsxs)(`div`,{className:`batch-links`,children:[r&&(0,x.jsx)(`button`,{className:`text-button`,"aria-expanded":!c,"aria-label":`${c?`展开`:`折叠`}子批次 #${n.id}`,onClick:()=>s(c?o.filter(e=>e!==i):[...o,i]),children:c?`展开子批次`:`折叠子批次`}),n.id!==e&&(0,x.jsxs)(`button`,{className:`text-button`,"aria-label":`查看批次 #${n.id}`,onClick:()=>t(n.id),children:[`查看详情`,n.can_relearn&&!r?` / 重新学习`:``,` `,`→`]})]}),(0,x.jsx)(_r,{id:n.id})]},n.id)})}),u.total>u.limit&&(0,x.jsxs)(`div`,{className:`learning-pagination tree-pagination`,children:[(0,x.jsx)(`button`,{disabled:u.offset===0,onClick:()=>{a(Math.max(0,u.offset-u.limit)),s([])},children:`上一页节点`}),(0,x.jsx)(`button`,{disabled:u.offset+u.items.length>=u.total,onClick:()=>{a(u.offset+u.limit),s([])},children:`下一页节点`})]}),(0,x.jsx)(`p`,{className:`muted`,children:`分页统计的是批次树节点;实际模型调用次数在各节点历史中单独显示。`})]}):(0,x.jsx)(`p`,{children:`正在读取拆分树…`})]})]})}var yr={topic:`常聊话题`,saying:`群内说法`,rule:`明确群规`};function br({value:e}){return e.kind?(0,x.jsxs)(`div`,{className:`profile-item-content`,children:[(0,x.jsxs)(`p`,{children:[(0,x.jsx)(M,{children:yr[e.kind]||e.kind}),` `,(0,x.jsx)(`strong`,{children:e.label})]}),e.meaning&&(0,x.jsxs)(`p`,{children:[`含义:`,e.meaning]}),e.quotes?.map((e,t)=>(0,x.jsxs)(`blockquote`,{children:[e.text,(0,x.jsxs)(`footer`,{children:[`消息 #`,e.message_id,` ·`,` `,e.field===`content`?`消息正文`:e.field===`quote`?`转引原话`:`原话归属未记录`,` `,`· 原作者:`,e.speaker_name||e.speaker||`未记录`]})]},t))]}):(0,x.jsx)(`p`,{className:`entry-profile-content`,children:e.text})}var xr=e=>({small:`小`,medium:`中`,large:`大`})[e]||e,Sr=e=>({periodic:`定期更新`,regenerate:`重新生成`,edit:`管理员编辑`,rollback:`回滚`})[e]||e,Cr=e=>({published:`已发布`,candidate:`待确认`,rejected:`被拒绝`,failed:`失败`,conflict:`版本冲突`,skipped:`已跳过`})[e]||e,wr={message_cleaned:`原消息已清理`,message_changed:`原消息已变化`,memory_unavailable:`记忆已失效或删除`,memory_changed:`记忆已修改`,visibility_changed:`记忆可见范围已变化`,memory_sources_changed:`记忆来源已变化`,disabled:`本入口画像已关闭`,entry_kind:`私聊入口不生成画像`,daily_limit:`今天已经尝试过生成,请在下一个角色本地日期重试`,insufficient_messages:`首次生成需要最近 14 天至少 50 条消息`,no_evidence:`没有可用的本入口依据`,unconfigured:`尚未配置对话模型`,daily_token_limit:`达到今日 token 上限`,current_version_changed:`当前已发布版本已变化,结果未发布`,settings_changed:`画像设置已变化,结果未发布`,evidence_changed:`依据在生成期间发生变化,结果未发布`,interrupted:`服务中断,任务未完成`,unexpected_error:`生成失败,请查看运行状态`,timeout:`模型调用超时`,invalid_output:`模型输出格式无效,未发布`,invalid_sentence:`候选句格式无效`,too_many_deleted:`检查删除了超过 30% 的候选句`,too_short:`正文低于旧版摘要的最少字数要求`,too_long:`正文超过该版本的字数上限`,superseded:`已被新候选或编辑取代`,paused:`对话用途已暂停`,invalid_items:`候选条目列表格式无效`,invalid_item:`条目类型、名称或含义格式无效`,invalid_quotes:`每个模型条目需要 1—3 条引语`,invalid_quote:`引语格式无效或超过 100 字`,invalid_quote_reference:`引语消息编号不在本入口材料中`,quote_not_found:`引语不是消息正文或转引原话的连续片段`,invalid_evidence_reference:`含义引用的记忆依据无效`,duplicate_item:`条目重复`,account_identifier:`包含账号标识或长数字号段`,insufficient_topic_recurrence:`常聊话题缺少跨日期的独立消息正文依据`,too_many_items:`条目数超过 6 条(包含手写条目)`,token_budget:`超过画像的估算 token 预算`,empty:`检查后没有可保留内容`,administrator_rejected:`管理员拒绝`},Tr=(e,t)=>e===`too_many_deleted`?t===2?`检查累计删除了超过 50% 的模型条目`:t===1?`检查删除了超过 30% 的候选句`:`检查删除比例超出该版本上限,请查看版本检查记录`:wr[e]||e,Er=e=>e.memory_id?`记忆 #${e.memory_id} · 修订 ${e.revision}`:`消息 #${e.message_id}`,Dr={evidence_budget_exceeded:`必要依据超出单次检查预算,暂不能完成这次检查`,basis_changed:`依据已变化,需要重新检查`,reject:`本次自动检查未通过`,invalid_output:`模型返回的检查结果格式无效,未据此准入`,timeout:`本次模型调用超时,已保留此前处理结果`,rule_scope_not_established:`尚未建立这条群规的适用范围依据`,bundle_rejected:`同组检查已有页面未通过,后续页已取消`,stale_check:`检查所用的正文或依据已变化,需要重新检查`,call_budget:`本轮调用额度已用完,剩余工作留待后续处理`,purged:`相关内容已彻底清除,本项停止处理`,merged_basis:`合并后的依据需要重新检查`,no_new_evidence:`没有新依据,本次跳过;不会重复增加保留强度。`,snapshot_changed:`依据或画像已变化,本次结果未写入。`},Or=e=>e.startsWith(`unbound_entities:`)?`相关对象缺少绑定依据:${e.slice(17).split(`,`).join(`、`)}`:Dr[e]||Tr(e),kr=e=>({manual:`人工条目`,candidate:`候选`,pending:`等待自动检查`,accepted:`已准入`,rejected:`检查未通过`})[e]||e;function Ar({base:e,openMemory:t}){let[n,r]=(0,l.useState)(0),i=y(`${e}/versions?limit=10&offset=${n}`),[a,o]=(0,l.useState)(null),s=(0,l.useRef)(null),c=y(a===null?null:`${e}/versions/${a}`),[u,d]=(0,l.useState)(`1`),[f,p]=(0,l.useState)(`2`),[m,h]=(0,l.useState)(null),g=y(m),_=e=>{o(e),requestAnimationFrame(()=>s.current?.scrollIntoView?.({behavior:`smooth`,block:`start`}))};return(0,x.jsxs)(x.Fragment,{children:[(0,x.jsxs)(`section`,{className:`entry-profile-section`,"aria-label":`历史(旧形式)`,children:[(0,x.jsx)(`h3`,{children:`历史(旧形式)`}),(0,x.jsx)(`p`,{className:`muted`,children:`旧版本只读,保留原有依据与检查记录;不能回滚,也不再确认或拒绝候选。这里的版本号与宿主投影版本无关。`}),i.error&&(0,x.jsxs)(A,{error:!0,children:[i.error,(0,x.jsx)(`button`,{onClick:i.refresh,children:`重试历史`})]}),i.data?(0,x.jsxs)(x.Fragment,{children:[(0,x.jsx)(`ul`,{className:`entry-profile-history`,children:i.data.items.map(e=>(0,x.jsxs)(`li`,{children:[(0,x.jsxs)(`div`,{children:[(0,x.jsxs)(`strong`,{children:[`v`,e.version]}),` · `,Cr(e.status),` ·`,` `,Sr(e.source),` · 变化`,xr(e.change_degree),(0,x.jsx)(`p`,{className:`muted`,children:w(e.generated_at)})]}),(0,x.jsxs)(`button`,{className:`secondary`,onClick:()=>_(e.version),children:[`查看旧版 v`,e.version]})]},e.version))}),!i.data.items.length&&(0,x.jsx)(`p`,{children:`还没有版本记录。`}),(0,x.jsx)(T,{total:i.data.total,offset:n,limit:10,change:r})]}):(0,x.jsx)(`p`,{children:`正在读取历史…`}),(0,x.jsxs)(`form`,{className:`entry-profile-compare`,onSubmit:t=>{t.preventDefault(),h(`${e}/diff?before_version=${Number(u)}&after_version=${Number(f)}`),g.refresh()},children:[(0,x.jsxs)(`label`,{children:[`对比起始版本`,(0,x.jsx)(`input`,{type:`number`,required:!0,min:1,max:2**53-1,value:u,onChange:e=>d(e.target.value)})]}),(0,x.jsxs)(`label`,{children:[`对比目标版本`,(0,x.jsx)(`input`,{type:`number`,required:!0,min:1,max:2**53-1,value:f,onChange:e=>p(e.target.value)})]}),(0,x.jsx)(`button`,{children:`逐句对比`})]}),g.error&&(0,x.jsx)(A,{error:!0,children:g.error}),g.data&&(0,x.jsxs)(`div`,{"aria-label":`逐句差异`,children:[!g.data.changed&&(0,x.jsx)(`p`,{children:`两版内容与依据相同。`}),g.data.sentences.map((e,t)=>(0,x.jsxs)(`div`,{className:`entry-profile-diff ${e.op}`,children:[(0,x.jsx)(`strong`,{children:{equal:JSON.stringify(e.before)===JSON.stringify(e.after)?`保留`:`依据或手写标记变化`,replace:`改写`,insert:`新增`,delete:`删除`}[e.op]||e.op}),e.op===`equal`&&JSON.stringify(e.before)===JSON.stringify(e.after)?e.after.map((e,t)=>(0,x.jsx)(Mr,{value:e},t)):(0,x.jsxs)(`div`,{className:`entry-profile-diff-columns`,children:[(0,x.jsxs)(`div`,{children:[(0,x.jsx)(`h4`,{children:`起始版本`}),e.before.map((e,t)=>(0,x.jsx)(Mr,{value:e},t))]}),(0,x.jsxs)(`div`,{children:[(0,x.jsx)(`h4`,{children:`目标版本`}),e.after.map((e,t)=>(0,x.jsx)(Mr,{value:e},t))]})]})]},t))]})]}),a!==null&&(0,x.jsxs)(`section`,{ref:s,className:`entry-profile-section`,"aria-label":`旧版详情`,children:[(0,x.jsxs)(`h3`,{children:[`旧版 v`,a,` 的依据与检查`]}),(0,x.jsx)(M,{children:`只读历史`}),c.error?(0,x.jsxs)(A,{error:!0,children:[c.error,(0,x.jsx)(`button`,{onClick:c.refresh,children:`重试版本详情`})]}):c.data?(0,x.jsx)(jr,{value:c.data,openMemory:t}):(0,x.jsx)(`p`,{children:`正在读取版本…`})]})]})}function jr({value:e,openMemory:t}){let n=e.checks,r=n.model,i=n.format_version||1,a=n.reason!==`invalid_output`&&r&&typeof r.reason==`string`&&Array.isArray(r.sentences)&&r.sentences.length===n.checked_sentences?.length&&r.sentences.every((e,t)=>e&&typeof e==`object`&&e.index===t+1&&typeof e.supported==`boolean`&&typeof e.scene_qualified==`boolean`&&typeof e.reason==`string`&&Array.isArray(e.violations)&&e.violations.every(e=>typeof e==`string`));return(0,x.jsxs)(x.Fragment,{children:[(0,x.jsxs)(`p`,{children:[Cr(e.status),` · `,Sr(e.source),` · 变化`,xr(e.change_degree),` · `,w(e.generated_at)]}),i===1&&(0,x.jsx)(M,{children:`旧版摘要`}),e.redacted&&(0,x.jsxs)(A,{children:[`此旧版本的相关内容已彻底清除。`,e.redaction_reason&&`原因:${e.redaction_reason}`]}),e.possibly_stale&&(0,x.jsx)(A,{children:`此版本依据可能过时。`}),(0,x.jsx)(`ol`,{className:`entry-profile-sentences`,children:e.sentences.map((e,n)=>(0,x.jsxs)(`li`,{children:[(0,x.jsx)(br,{value:e}),(0,x.jsx)(M,{children:e.admin_written?`管理员手写 · 受保护`:`模型生成`}),e.admin_written&&!e.basis.length&&(0,x.jsx)(`p`,{className:`muted`,children:`管理员直接提供,无模型依据。`}),e.basis.map((e,n)=>(0,x.jsxs)(`div`,{className:`entry-profile-evidence`,children:[e.memory_id?(0,x.jsx)(`button`,{className:`text-button`,onClick:()=>t(e.memory_id),children:Er(e)}):(0,x.jsx)(`strong`,{children:Er(e)}),e.evidence?(0,x.jsxs)(x.Fragment,{children:[(0,x.jsx)(`p`,{children:e.evidence.content}),e.evidence.occurred_at&&(0,x.jsx)(`small`,{children:w(e.evidence.occurred_at)}),e.evidence.source_messages?.map(e=>(0,x.jsxs)(`blockquote`,{children:[e.content,(0,x.jsxs)(`footer`,{children:[`本入口原消息 #`,e.message_id,` ·`,` `,w(e.occurred_at)]})]},e.message_id))]}):(0,x.jsx)(`p`,{children:`此版本未保存来源摘录,可按依据编号核对历史。`}),e.memory&&(0,x.jsxs)(`small`,{children:[`当前修订 `,e.memory.revision,` ·`,` `,{active:`有效`,forgotten:`已遗忘`,deleted:`已删除`}[e.memory.lifecycle]||e.memory.lifecycle]})]},n))]},n))}),(0,x.jsxs)(`h4`,{children:[`检查结果:`,n.passed?`通过`:`未通过`]}),n.reason&&(0,x.jsx)(`p`,{children:Tr(n.reason,i)}),n.quote_validation&&(0,x.jsxs)(`p`,{children:[`引用原话核对:`,n.quote_validation.passed,` /`,` `,n.quote_validation.total,` `,`条通过(含被删条目的引语;不代表语义支持)`]}),!!n.quote_validation_items?.length&&(0,x.jsxs)(`details`,{children:[(0,x.jsx)(`summary`,{children:`逐条引用核对`}),(0,x.jsx)(`ul`,{children:n.quote_validation_items.map(e=>(0,x.jsxs)(`li`,{children:[`候选第 `,e.index,` 项:`,e.passed,` / `,e.total,` 条原话通过`,e.quotes.filter(e=>!e.passed).map(e=>(0,x.jsxs)(`p`,{children:[`引语 `,e.index,`:`,Tr(e.reason||`invalid_quote`,i)]},e.index))]},e.index))})]}),n.deterministic?.characters!=null&&(0,x.jsxs)(`p`,{children:[`正文 `,n.deterministic.characters,` 字 ·`,` `,n.deterministic.tokens??`未记录`,` 估算 token`]}),(0,x.jsxs)(`p`,{children:[`确定性检查:`,n.deterministic?.passed?`通过`:`存在删句或错误,见下方记录`]}),n.errors?.length?(0,x.jsx)(`ul`,{children:n.errors.map((e,t)=>(0,x.jsx)(`li`,{children:Tr(e,i)},t))}):null,a&&r?(0,x.jsxs)(x.Fragment,{children:[(0,x.jsxs)(`p`,{children:[`模型判断:`,r.reason]}),(0,x.jsx)(`ol`,{children:r.sentences.map((e,t)=>(0,x.jsxs)(`li`,{children:[(0,x.jsx)(`p`,{children:n.checked_sentences?.[t]?.text}),(0,x.jsxs)(`p`,{children:[`依据`,e.supported?`支持`:`不支持`,` · 场景限定`,e.scene_qualified?`通过`:`未通过`,` ·`,` `,n.checked_sentences?.[t]?.meaning&&typeof e.meaning_supported==`boolean`?`含义${e.meaning_supported?`有依据`:`无依据,已移除`} · `:``,e.violations.length?e.violations.join(`、`):`未发现禁止内容`]}),(0,x.jsx)(`p`,{children:e.reason})]},e.index))})]}):(0,x.jsx)(`p`,{children:r?`模型检查输出格式无效,本版未通过检查。`:n.admin_edit?`管理员编辑只做确定性检查。`:`本版未执行模型检查。`}),!!n.removed_meanings?.length&&(0,x.jsxs)(x.Fragment,{children:[(0,x.jsx)(`h4`,{children:`已删含义记录`}),(0,x.jsx)(`p`,{children:`说法本身可以保留;没有含义不表示生成失败。`}),(0,x.jsx)(`ul`,{children:n.removed_meanings.map((e,t)=>(0,x.jsxs)(`li`,{children:[`候选第 `,e.index,` 项:`,e.meaning,(0,x.jsx)(`p`,{children:Tr(e.reason,i)})]},t))})]}),!!n.admin_warnings?.length&&(0,x.jsxs)(x.Fragment,{children:[(0,x.jsx)(`h4`,{children:`手写条目检查提醒`}),(0,x.jsx)(`ul`,{children:n.admin_warnings.map((e,t)=>(0,x.jsxs)(`li`,{children:[`第 `,e.index,` 项:`,e.result.violations.join(`、`),` ·`,` `,e.result.reason]},t))})]}),!!n.deleted_sentences?.length&&(0,x.jsxs)(x.Fragment,{children:[(0,x.jsx)(`h4`,{children:i===2?`已删条目记录`:`已删句记录`}),(0,x.jsx)(`ul`,{children:n.deleted_sentences.map((e,t)=>(0,x.jsxs)(`li`,{children:[(0,x.jsx)(`p`,{children:typeof e.sentence?.text==`string`?e.sentence.text:`候选第 ${e.index} 项`}),(0,x.jsxs)(`p`,{children:[e.stage===`deterministic`?`引用与格式核对:`:e.stage===`model`?`模型检查:`:``,Tr(e.reason,i)]})]},t))})]})]})}function Mr({value:e}){return(0,x.jsxs)(`div`,{children:[(0,x.jsx)(br,{value:e}),(0,x.jsxs)(`small`,{children:[e.author===`admin`?`管理员手写`:`模型生成`,e.basis?.length?` · ${e.basis.map(Er).join(`;`)}`:``]})]})}var Nr={topic:`话题`,saying:`说法`,rule:`群规`},Pr=e=>e?Nr[e]:`管理员说明`;function Fr({entry:e,blocked:t,busy:n,write:r,cancel:i,created:a}){let[o,s]=(0,l.useState)(`rule`),[c,u]=(0,l.useState)(``),[d,f]=(0,l.useState)(``);return(0,x.jsxs)(`form`,{className:`profile-memory-card`,"aria-label":`人工新建画像记忆`,onSubmit:async n=>{if(n.preventDefault(),!(t||!c.trim()||Array.from(c).length>1e3)){f(``);try{await r(`/memories`,`POST`,{kind:`入口画像`,owner_entry_id:e.id,category:o,content:c}),a()}catch(e){f(v(e))}}},children:[(0,x.jsx)(`h4`,{children:`人工新建画像记忆`}),(0,x.jsxs)(`p`,{children:[`仅用于入口“`,e.name,`”。人工内容无需模型依据。`]}),d&&(0,x.jsx)(A,{error:!0,children:d}),(0,x.jsxs)(`label`,{children:[`子类`,(0,x.jsxs)(`select`,{"aria-label":`子类`,value:o||``,disabled:n,onChange:e=>s(e.target.value||null),children:[Object.entries(Nr).map(([e,t])=>(0,x.jsx)(`option`,{value:e,children:t},e)),(0,x.jsx)(`option`,{value:``,children:`管理员说明`})]})]}),(0,x.jsxs)(`label`,{children:[`新画像正文`,(0,x.jsx)(`textarea`,{"aria-label":`新画像正文`,rows:4,maxLength:1e3,value:c,disabled:n,onChange:e=>u(e.target.value)})]}),(0,x.jsxs)(`p`,{className:`fine-print`,children:[Array.from(c).length,` / 1000 字`]}),(0,x.jsxs)(`div`,{className:`button-row`,children:[(0,x.jsx)(`button`,{disabled:t||!c.trim()||Array.from(c).length>1e3,children:`创建画像记忆`}),(0,x.jsx)(`button`,{type:`button`,className:`secondary`,disabled:n,onClick:i,children:`取消新建`})]})]})}function Ir({id:e,revision:t,message:n,openMemory:r,speakerNames:i,pendingSources:a}){let o=y(`/memories/${e}`),s=(0,l.useRef)(null);return(0,l.useEffect)(()=>{o.refresh()},[t,o.refresh]),(0,l.useEffect)(()=>{o.data&&n!==null&&(s.current?.scrollIntoView?.({behavior:`smooth`,block:`start`}),s.current?.focus({preventScroll:!0}))},[o.data,n]),(0,x.jsxs)(`div`,{className:`profile-memory-evidence`,children:[o.error&&(0,x.jsxs)(A,{error:!0,children:[o.error,(0,x.jsx)(`button`,{onClick:o.refresh,children:`重试来源与修订`})]}),o.data?(0,x.jsxs)(x.Fragment,{children:[(0,x.jsx)(`h4`,{children:`来源消息与前后文`}),!o.data.sources.length&&(0,x.jsx)(`p`,{children:`没有保存的来源消息。`}),o.data.sources.map(e=>(0,x.jsxs)(`section`,{ref:e.message_id===n?s:null,tabIndex:-1,className:e.message_id===n?`profile-source-target`:``,"aria-label":e.message_id?`来源消息 #${e.message_id}`:`来源 #${e.id}`,children:[e.message_id&&(0,x.jsxs)(`h5`,{children:[`消息 #`,e.message_id]}),a.some(t=>t.message.position===e.message_id)&&(0,x.jsx)(A,{children:`该消息含待审引文,请以上方已审/待审分区为准`}),(0,x.jsx)(vn,{source:e,openMemory:r,initiallyOpen:e.message_id===n,speakerNames:i},`${e.id}:${e.message_id===n}`)]},e.id)),n!==null&&!o.data.sources.some(e=>e.message_id===n)&&(0,x.jsx)(A,{children:`该来源已变化或不可用,请刷新画像核对。`}),(0,x.jsxs)(`h4`,{children:[`修订历史 · `,o.data.revisions.length]}),o.data.revisions.length?o.data.revisions.map(e=>(0,x.jsx)(yn,{revision:e},e.id)):(0,x.jsx)(`p`,{children:`还没有修订记录。`})]}):!o.error&&(0,x.jsx)(`p`,{children:`正在读取来源与修订…`})]})}function Lr({memory:e,speakerNames:t,displayed:n,blocked:r,busy:i,write:a,refresh:o,openMemory:s}){let[c,u]=(0,l.useState)(null),[d,p]=(0,l.useState)(null),[m,h]=(0,l.useState)(!1),[g,_]=(0,l.useState)(``),[y,b]=(0,l.useState)(``),[S,C]=(0,l.useState)(!1),[T,D]=(0,l.useState)(null),O=!!c&&c.revision!==e.revision,k=e.lifecycle===`deleted`;function j(e){e instanceof f&&e.status===409?(h(!0),_(`这条画像记忆已变化,草稿已保留。请核对最新正文与修订后继续。`)):e instanceof f&&e.status===404?(h(!0),_(`这条画像记忆已不可用,草稿已保留。请复制需要保留的内容后关闭。`)):_(v(e))}async function ee(t,n,i,o){if(!r){_(``),b(``);try{await a(`/memories/${e.memory_id}${n}`,t,i),u(null),p(null),h(!1),b(o)}catch(e){j(e)}}}return(0,x.jsxs)(`article`,{id:`profile-memory-${e.memory_id}`,className:`profile-memory-card`,"aria-label":`画像记忆 #${e.memory_id}`,children:[(0,x.jsxs)(`div`,{className:`detail-badges`,children:[(0,x.jsx)(M,{tone:`purple`,children:Pr(e.category)}),(0,x.jsx)(M,{children:e.author===`admin`?`人工`:`模型`}),(0,x.jsx)(M,{children:kr(e.admission_state)}),e.manual_protected&&(0,x.jsx)(M,{children:`人工保护`}),(0,x.jsx)(M,{tone:k?`danger`:``,children:E(e.lifecycle)}),(0,x.jsxs)(`span`,{className:`muted`,children:[`#`,e.memory_id,` · 修订 `,e.revision]})]}),(0,x.jsx)(`p`,{className:`entry-profile-content`,children:e.body}),e.admission_state===`candidate`&&(0,x.jsx)(A,{children:`还在等待更多独立讨论,暂不给宿主看`}),e.admission_state===`pending`&&(0,x.jsx)(A,{children:`正在排队或自动检查,全部检查页通过前暂不给宿主看。`}),e.admission_state===`rejected`&&(0,x.jsx)(A,{children:`自动检查未通过,暂不给宿主看;后续由系统继续处理新依据。`}),e.admission_reason&&(0,x.jsxs)(`div`,{children:[(0,x.jsx)(`p`,{children:Or(e.admission_reason)}),e.admission_state===`manual`&&(0,x.jsx)(`p`,{className:`fine-print`,children:`这是自动依据检查记录,不影响人工正文的使用。`}),e.admission_state===`accepted`&&(0,x.jsx)(`p`,{className:`fine-print`,children:`后续检查记录不代表已准入正文被撤下,是否展示请看宿主投影。`})]}),(0,x.jsxs)(`dl`,{className:`profile-memory-metadata`,children:[(0,x.jsxs)(`div`,{children:[(0,x.jsx)(`dt`,{children:`保留强度`}),(0,x.jsxs)(`dd`,{children:[e.retention,e.pinned?` · 已置顶`:``]})]}),(0,x.jsxs)(`div`,{children:[(0,x.jsx)(`dt`,{children:`最近确认时间`}),(0,x.jsx)(`dd`,{children:e.last_confirmed_at?w(e.last_confirmed_at):`暂无确认记录`})]}),(0,x.jsxs)(`div`,{children:[(0,x.jsx)(`dt`,{children:`独立日期`}),(0,x.jsxs)(`dd`,{children:[e.independent_days,` 天`]})]}),(0,x.jsxs)(`div`,{children:[(0,x.jsx)(`dt`,{children:`独立依据`}),(0,x.jsxs)(`dd`,{children:[e.independent_uses,` 次`]})]}),(0,x.jsxs)(`div`,{children:[(0,x.jsx)(`dt`,{children:`宿主投影`}),(0,x.jsx)(`dd`,{children:n?`已加入`:e.usable?`当前未加入`:`当前不可用`})]})]}),(0,x.jsx)(`p`,{className:`fine-print`,children:`独立日期按角色时区统计;独立依据已排除转引、复述等重复讨论,不等于消息总数。`}),(0,x.jsxs)(`div`,{"aria-label":`依据原话`,children:[(0,x.jsxs)(`h4`,{children:[`已审依据 · `,e.sources.length,` 条`]}),e.sources.map((e,n)=>(0,x.jsxs)(`blockquote`,{children:[(0,x.jsx)(`p`,{children:e.excerpt}),(0,x.jsxs)(`footer`,{children:[e.field===`quote`?`转引原话`:`消息正文`,` · 原作者:`,t[e.speaker]||e.speaker,` ·`,` `,w(e.message.occurred_at)]}),(0,x.jsxs)(`button`,{className:`text-button`,disabled:i,onClick:()=>{D(e.message.position),C(!0)},children:[`跳转来源消息 #`,e.message.position]})]},`${e.ref}:${n}`)),!e.sources.length&&(0,x.jsx)(`p`,{className:`muted`,children:e.author===`admin`?`管理员手写,无需模型依据。`:`当前没有可用的绑定原话,可在来源与修订历史中核对。`})]}),!!e.pending_sources.length&&(0,x.jsxs)(`section`,{"aria-label":`待审来源`,className:`profile-pending-sources`,children:[(0,x.jsxs)(`h4`,{children:[`待审来源 · `,e.pending_sources.length,` 条`]}),(0,x.jsxs)(`p`,{className:`fine-print`,children:[`这些引文尚未完成自动准入检查,不作为已审依据。`,e.admission_state===`manual`?`人工正文照常可用,无需等待这些引文。`:`通过检查后才可能成为可展示画像的依据。`]}),e.pending_sources.map((e,n)=>(0,x.jsxs)(`blockquote`,{children:[(0,x.jsx)(`p`,{children:e.excerpt}),(0,x.jsxs)(`footer`,{children:[e.field===`quote`?`转引原话`:`消息正文`,` · 原作者:`,t[e.speaker]||e.speaker,` ·`,` `,w(e.message.occurred_at)]}),(0,x.jsxs)(`button`,{className:`text-button`,disabled:i,onClick:()=>{D(e.message.position),C(!0)},children:[`跳转来源消息 #`,e.message.position]})]},`${e.ref}:${n}`))]}),g&&(0,x.jsxs)(A,{error:!0,children:[g,(0,x.jsx)(`button`,{className:`text-button`,disabled:i,onClick:()=>{o(),!c&&d===null&&(h(!1),_(``))},children:`刷新画像记忆`})]}),y&&(0,x.jsx)(A,{children:y}),c&&(0,x.jsxs)(`form`,{onSubmit:e=>{e.preventDefault(),!r&&!m&&!O&&!k&&c.content.trim()&&ee(`PATCH`,``,{expected_revision:c.revision,content:c.content},`修改已保存。`)},children:[(0,x.jsxs)(`label`,{children:[`画像正文`,(0,x.jsx)(`textarea`,{"aria-label":`画像正文`,value:c.content,disabled:i,rows:4,maxLength:1e3,onChange:e=>u({...c,content:e.target.value})})]}),(0,x.jsxs)(`p`,{className:`fine-print`,children:[`草稿基于修订 `,c.revision,` · `,Array.from(c.content).length,` / 1000 字`]}),(O||m)&&(0,x.jsxs)(A,{children:[`草稿未被替换。上方显示当前修订 `,e.revision,` `,`的正文,请核对后继续。`,(0,x.jsx)(`button`,{type:`button`,disabled:r||k||m&&!O,onClick:()=>{u({...c,revision:e.revision}),h(!1),_(``)},children:`已核对最新修订,保留草稿继续`})]}),(0,x.jsxs)(`div`,{className:`button-row`,children:[(0,x.jsx)(`button`,{disabled:r||m||O||k||!c.content.trim()||Array.from(c.content).length>1e3,children:`保存修改`}),(0,x.jsx)(`button`,{className:`secondary`,type:`button`,disabled:i,onClick:()=>{u(null),h(!1),_(``)},children:`取消编辑`})]})]}),d!==null&&(0,x.jsxs)(`div`,{className:`delete-confirm`,children:[(0,x.jsx)(`p`,{children:`确认删除这条画像记忆?该标识将永久撤销,不能恢复;来源和修订历史保留。`}),d!==e.revision&&(0,x.jsx)(A,{children:`这条记忆已变化,请取消并核对最新正文后重新操作。`}),(0,x.jsxs)(`div`,{className:`button-row`,children:[(0,x.jsx)(`button`,{className:`danger-button`,disabled:r||m||k||d!==e.revision,onClick:()=>void ee(`DELETE`,``,{expected_revision:d},`画像记忆已删除,历史仍保留。`),children:`确认删除`}),(0,x.jsx)(`button`,{className:`secondary`,disabled:i,onClick:()=>{p(null),h(!1),_(``)},children:`取消删除`})]})]}),k&&(0,x.jsx)(`p`,{className:`muted`,children:`已删除的记忆标识不可恢复。需要重新手写时,请使用人工新建。`}),(0,x.jsxs)(`div`,{className:`button-row`,children:[!k&&(0,x.jsx)(`button`,{className:`secondary`,disabled:r||!!c||d!==null,onClick:()=>{u({content:e.body,revision:e.revision}),h(!1),_(``),b(``)},children:`编辑`}),!k&&(0,x.jsx)(`button`,{className:`text-button danger-text`,disabled:r||!!c||d!==null,onClick:()=>{p(e.revision),h(!1),_(``),b(``)},children:`删除`}),e.lifecycle===`forgotten`&&(0,x.jsx)(`button`,{className:`secondary`,disabled:r||!!c||d!==null||m,onClick:()=>void ee(`POST`,`/restore`,{expected_revision:e.revision},`画像记忆已恢复。`),children:`恢复`}),(0,x.jsx)(`button`,{className:`text-button`,"aria-expanded":S,disabled:i,onClick:()=>{C(!S),D(null)},children:S?`收起来源与修订历史`:`来源与修订历史`}),(0,x.jsx)(`button`,{className:`text-button`,disabled:i||!!c,onClick:()=>s(e.memory_id),children:`打开记忆详情`})]}),S&&(0,x.jsx)(Ir,{id:e.memory_id,speakerNames:t,revision:e.revision,pendingSources:e.pending_sources,message:T,openMemory:s})]})}function Rr({progress:e}){return e?(0,x.jsxs)(`section`,{"aria-label":`当前入口进度与积压`,children:[(0,x.jsx)(`h4`,{children:`当前入口进度与积压`}),(0,x.jsxs)(`dl`,{className:`profile-progress-grid`,children:[(0,x.jsxs)(`div`,{children:[(0,x.jsx)(`dt`,{children:`已完成观察分块`}),(0,x.jsxs)(`dd`,{children:[e.completed_chunks,` / `,e.observation_chunks,` 块`]})]}),(0,x.jsxs)(`div`,{children:[(0,x.jsx)(`dt`,{children:`待检查`}),(0,x.jsxs)(`dd`,{children:[e.pending_checks,` 页`]})]}),(0,x.jsxs)(`div`,{children:[(0,x.jsx)(`dt`,{children:`检查失败`}),(0,x.jsxs)(`dd`,{children:[e.failed_checks,` 页`]})]}),(0,x.jsxs)(`div`,{children:[(0,x.jsx)(`dt`,{children:`检查未通过`}),(0,x.jsxs)(`dd`,{children:[e.rejected_checks,` 页`]})]}),(0,x.jsxs)(`div`,{children:[(0,x.jsx)(`dt`,{children:`未准入画像`}),(0,x.jsxs)(`dd`,{children:[e.waiting_candidates,` 条`]})]}),(0,x.jsxs)(`div`,{children:[(0,x.jsx)(`dt`,{children:`待扫描`}),(0,x.jsxs)(`dd`,{children:[e.unscanned_messages,` 条消息`]})]}),(0,x.jsxs)(`div`,{children:[(0,x.jsx)(`dt`,{children:`观察缺口`}),(0,x.jsxs)(`dd`,{children:[e.observation_gaps,` 块`]})]}),(0,x.jsxs)(`div`,{children:[(0,x.jsx)(`dt`,{children:`最早积压时间`}),(0,x.jsx)(`dd`,{children:e.oldest_pending_at?w(e.oldest_pending_at):`无`})]}),(0,x.jsxs)(`div`,{children:[(0,x.jsx)(`dt`,{children:`已排队至`}),(0,x.jsxs)(`dd`,{children:[`消息 #`,e.scan_cursor]})]}),(0,x.jsxs)(`div`,{children:[(0,x.jsx)(`dt`,{children:`已连续完成至`}),(0,x.jsxs)(`dd`,{children:[`消息 #`,e.applied_cursor]})]})]}),(0,x.jsx)(`p`,{className:`fine-print`,children:`页数和记忆条目数分别统计;未准入不等于失败。消息编号表示处理位置,不是发生时间;连续完成位置不会越过观察缺口。`})]}):null}function zr(e){return e.state===`applied`?e.kind===`admit`?`本页通过`:`已完成`:e.state===`rejected`?e.kind===`admit`?`本页未通过`:`未通过`:{queued:`排队中`,running:`处理中`,failed:`本次失败`,cancelled:`已取消`,deferred:`稍后重试`}[e.state]||e.state}function Br({work:e}){return e?.length?(0,x.jsxs)(`details`,{className:`history profile-work-list`,children:[(0,x.jsxs)(`summary`,{children:[`本次任务分块与检查 · `,e.length,` 项`]}),(0,x.jsx)(`p`,{className:`fine-print`,children:`每页单独检查;全部页通过后才可能准入,还需核对最终正文与依据。宿主是否能看到,以每条画像的投影状态为准。`}),e.map(e=>(0,x.jsxs)(`article`,{"aria-label":`工作项 #${e.id}`,className:`profile-work-card`,children:[(0,x.jsxs)(`div`,{className:`detail-badges`,children:[(0,x.jsx)(`strong`,{children:e.kind===`observe`?`观察消息分块`:`画像准入检查`}),(0,x.jsx)(M,{tone:e.state===`failed`||e.state===`rejected`?`danger`:``,children:zr(e)}),(0,x.jsxs)(`span`,{className:`muted`,children:[`工作项 #`,e.id]})]}),e.memory_id!=null&&(0,x.jsxs)(`button`,{className:`text-button`,onClick:()=>document.getElementById(`profile-memory-${e.memory_id}`)?.scrollIntoView?.({behavior:`smooth`,block:`start`}),children:[`画像记忆 #`,e.memory_id]}),e.kind===`admit`&&(0,x.jsx)(`p`,{children:e.check_page!=null&&e.check_pages!=null?`检查第 ${e.check_page} / ${e.check_pages} 页`:`检查页码未记录`}),e.kind===`admit`&&e.state===`applied`&&(0,x.jsx)(`p`,{className:`fine-print`,children:`全部页通过后才可能准入,本页通过不代表整条已展示。`}),(0,x.jsxs)(`dl`,{className:`profile-work-metadata`,children:[(0,x.jsxs)(`div`,{children:[(0,x.jsx)(`dt`,{children:`所属日期(角色时区)`}),(0,x.jsx)(`dd`,{children:e.local_day||`未记录`})]}),e.kind===`observe`&&(0,x.jsxs)(`div`,{children:[(0,x.jsx)(`dt`,{children:`消息编号范围`}),(0,x.jsxs)(`dd`,{children:[`#`,e.cursor_before,` 之后至 #`,e.cursor_after]})]}),(0,x.jsxs)(`div`,{children:[(0,x.jsx)(`dt`,{children:`执行次数`}),(0,x.jsxs)(`dd`,{children:[e.attempts,` 次`]})]}),(0,x.jsxs)(`div`,{children:[(0,x.jsx)(`dt`,{children:`下次重试`}),(0,x.jsx)(`dd`,{children:e.retry_at?w(e.retry_at):`未安排`})]}),(0,x.jsxs)(`div`,{children:[(0,x.jsx)(`dt`,{children:`入队时间`}),(0,x.jsx)(`dd`,{children:w(e.created_at)})]}),e.finished_at&&(0,x.jsxs)(`div`,{children:[(0,x.jsx)(`dt`,{children:`结束时间`}),(0,x.jsx)(`dd`,{children:w(e.finished_at)})]})]}),e.reason&&(0,x.jsx)(`p`,{children:Or(e.reason)})]},e.id))]}):null}function Vr({base:e,initial:t,settled:n,progress:r}){let[i,a]=(0,l.useState)(t.state===`running`),o=y(`${e}/attempts/${t.id}`,i?1500:0),s=(0,l.useRef)(!1);(0,l.useEffect)(()=>{o.data&&o.data.state!==`running`&&!s.current&&(s.current=!0,a(!1),n(o.data))},[o.data,n]);let c=i&&o.data||t;return(0,x.jsxs)(`section`,{className:`entry-profile-task`,"aria-label":`画像更新任务`,children:[(0,x.jsxs)(`h3`,{children:[`更新任务 #`,c.id]}),o.error&&(0,x.jsxs)(A,{error:!0,children:[o.error,(0,x.jsx)(`button`,{onClick:o.refresh,children:`重试任务状态`})]}),(0,x.jsx)(`p`,{role:`status`,children:c.state===`running`?c.stage===`checking`?`正在检查画像条目…`:`正在观察消息分块…`:c.state===`published`?`本轮已提交处理结果,仍以逐条准入状态为准。`:Cr(c.state)}),c.reason&&(0,x.jsx)(`p`,{children:Or(c.reason)}),(0,x.jsxs)(`p`,{className:`fine-print`,children:[`开始:`,w(c.started_at),c.finished_at&&` · 结束:${w(c.finished_at)}`]}),(0,x.jsx)(Rr,{progress:c.progress||r}),(0,x.jsx)(Br,{work:c.work}),c.version!=null&&(0,x.jsxs)(`p`,{children:[`任务完成时的宿主投影版本:v`,c.version]})]})}function Hr({entry:e,onClose:t,openMemory:n}){let r=`/entries/${encodeURIComponent(e.id)}/profile`,i=y(r,5e3),a=y(`/catalog`),o=Object.fromEntries((a.data?.people||[]).map(e=>[e.id,e.name])),[s,c]=(0,l.useState)(!1),[u,d]=(0,l.useState)(``),[p,m]=(0,l.useState)(``),[h,b]=(0,l.useState)(!1),[S,C]=(0,l.useState)(null),[T,E]=(0,l.useState)(null),D=(0,l.useRef)(!1),O=(0,l.useRef)(!0);(0,l.useEffect)(()=>(O.current=!0,()=>{O.current=!1}),[]);let k=async(e,t,n)=>{if(D.current)throw new f(`busy`,`正在保存,请稍候。`,409);D.current=!0,c(!0),d(``),m(``);try{return await g(e,_(t,n))}finally{D.current=!1,O.current&&(c(!1),i.refresh())}},j=(0,l.useCallback)(e=>{E(e),i.refresh()},[i.refresh]),N=i.data,P=N?.latest_attempt&&(!T||N.latest_attempt.id>T.id||N.latest_attempt.id===T.id&&(T.state===`running`||N.latest_attempt.state!==`running`))?N.latest_attempt:T||N?.latest_attempt,F=s||i.loading||!!i.error,I=P?.state===`running`,te=e=>{s||(t(),n(e))};async function L(){if(S&&!F)try{await k(`${r}/settings`,`PATCH`,S),C(null),m(`画像设置已保存。`)}catch(e){d(e instanceof f&&e.status===409?`画像已变化,设置草稿已保留;请核对最新投影后重新确认。`:v(e))}}async function ne(){if(!(!N||F||I))try{let e=await k(`${r}/regenerate`,`POST`,{expected_version:N.settings.current_version});E(e.attempt),m(`更新请求已提交,可关闭面板后再查看任务结果。`)}catch(e){d(v(e))}}return(0,x.jsx)(ee,{title:`入口画像 · ${e.name}`,onClose:t,closeDisabled:s,children:(0,x.jsxs)(`div`,{className:`entry-profile`,children:[(0,x.jsx)(`p`,{className:`muted`,children:`每条画像都是本入口的记忆,自动更新。手写内容按人工记忆保护。`}),(i.error||u)&&(0,x.jsxs)(A,{error:!0,children:[u||i.error,(0,x.jsx)(`button`,{className:`text-button`,disabled:s,onClick:i.refresh,children:`刷新画像`})]}),p&&(0,x.jsx)(A,{children:p}),N?N.supported?N.mode===`memory`?(0,x.jsxs)(x.Fragment,{children:[(0,x.jsxs)(`section`,{className:`entry-profile-section`,"aria-label":`画像设置`,children:[(0,x.jsxs)(`div`,{className:`button-row`,children:[(0,x.jsx)(M,{children:N.settings.enabled?`已开启`:`已关闭`}),(0,x.jsx)(`span`,{children:`自动更新,无需确认候选`})]}),!N.global_enabled&&(0,x.jsx)(A,{children:`全局画像开关已关闭,当前不会生成或加入回复准备。`}),S?(0,x.jsxs)(`form`,{onSubmit:e=>{e.preventDefault(),L()},children:[(0,x.jsxs)(`label`,{className:`check-label`,children:[(0,x.jsx)(`input`,{type:`checkbox`,checked:S.enabled,disabled:s,onChange:e=>C({...S,enabled:e.target.checked})}),`启用本入口画像`]}),S.expected_version!==N.settings.current_version&&(0,x.jsxs)(A,{children:[`投影已更新为 v`,N.settings.current_version,`,请先核对下方预览。`,(0,x.jsx)(`button`,{type:`button`,disabled:F,onClick:()=>C({...S,expected_version:N.settings.current_version}),children:`已核对,保留设置草稿`})]}),(0,x.jsxs)(`div`,{className:`button-row`,children:[(0,x.jsx)(`button`,{disabled:F||S.expected_version!==N.settings.current_version,children:`保存画像设置`}),(0,x.jsx)(`button`,{type:`button`,className:`secondary`,disabled:s,onClick:()=>C(null),children:`取消设置`})]})]}):(0,x.jsx)(`button`,{className:`secondary`,disabled:F,onClick:()=>C({enabled:N.settings.enabled,expected_version:N.settings.current_version}),children:`修改画像设置`})]}),(0,x.jsxs)(`section`,{className:`entry-profile-section`,"aria-label":`宿主投影预览`,children:[(0,x.jsxs)(`h3`,{children:[`宿主投影预览 · v`,N.settings.current_version]}),(0,x.jsx)(`p`,{className:`muted`,children:`以下为回复准备提供的原文;宿主传入已知记忆时会排除相应条目。投影版本独立于每条记忆的修订号。`}),N.projection?(0,x.jsxs)(x.Fragment,{children:[(0,x.jsx)(`p`,{className:`entry-profile-content profile-projection`,children:N.projection.text}),(0,x.jsxs)(`p`,{className:`fine-print`,children:[`version:`,N.projection.version,` · 更新于`,` `,w(N.projection.generated_at)]}),(0,x.jsx)(`div`,{className:`button-row`,children:N.projection.memory_ids.map(e=>(0,x.jsxs)(`button`,{className:`text-button`,onClick:()=>document.getElementById(`profile-memory-${e}`)?.scrollIntoView?.({behavior:`smooth`,block:`start`}),children:[`画像记忆 #`,e]},e))}),N.projection.possibly_stale&&(0,x.jsx)(A,{children:`投影可能过时:还有未处理消息、观察缺口、待检查或待重建的内容。`})]}):(0,x.jsx)(`p`,{children:`当前不向宿主提供画像(entry_profile 为 null)。`}),!!N.diagnostics.omitted_memory_ids?.length&&(0,x.jsxs)(A,{children:[`受展示预算限制,以下记忆未加入投影:`,N.diagnostics.omitted_memory_ids.map(e=>(0,x.jsxs)(`button`,{className:`text-button`,onClick:()=>document.getElementById(`profile-memory-${e}`)?.scrollIntoView?.({behavior:`smooth`,block:`start`}),children:[`#`,e]},e)),(0,x.jsx)(`p`,{children:`这些记忆仍然保留;人工内容不会因展示预算被隐藏。`})]}),N.rebuild_pending&&(0,x.jsx)(A,{children:`画像正在等待新任务重建或继续处理消息。`})]}),(0,x.jsxs)(`section`,{className:`entry-profile-section`,"aria-label":`每日增量进度`,children:[(0,x.jsx)(`h3`,{children:`每日增量进度`}),(0,x.jsx)(`p`,{className:`muted`,children:`自动任务按角色时区处理已结束日期;立即运行也处理当日尾块。准入由系统自动检查,无需审批。`}),(0,x.jsx)(`button`,{disabled:F||I||!N.settings.enabled||!N.global_enabled,onClick:()=>void ne(),children:`立即运行`}),(0,x.jsx)(`p`,{className:`fine-print`,children:`继续处理新消息和排队的检查;没有新依据时不会重复确认。`}),!P&&(0,x.jsx)(Rr,{progress:N.progress}),P&&(0,x.jsx)(Vr,{base:r,initial:P,settled:j,progress:N.progress},P.id)]}),(0,x.jsxs)(`section`,{className:`entry-profile-section`,"aria-label":`画像记忆`,children:[(0,x.jsxs)(`div`,{className:`panel-heading`,children:[(0,x.jsxs)(`h3`,{children:[`画像记忆 · `,N.memories.length,` 条`]}),(0,x.jsx)(`button`,{disabled:F||h,onClick:()=>b(!0),children:`人工新建`})]}),h&&(0,x.jsx)(Fr,{entry:e,blocked:F,busy:s,write:k,cancel:()=>b(!1),created:()=>{b(!1),m(`已创建本入口的画像记忆。`)}}),!N.memories.length&&(0,x.jsx)(`p`,{children:`暂无画像记忆。可手写补充,也可等待新消息形成画像。`}),(0,x.jsx)(`div`,{className:`profile-memory-list`,children:N.memories.map(e=>(0,x.jsx)(Lr,{memory:e,speakerNames:o,displayed:!!N.projection?.memory_ids.includes(e.memory_id),blocked:F,busy:s,write:k,refresh:i.refresh,openMemory:te},e.memory_id))})]})]}):(0,x.jsx)(A,{children:`入口画像尚未切换为记忆形式,请更新服务后刷新。旧版历史仍可只读查看。`}):(0,x.jsx)(A,{children:`私聊入口不生成入口画像。`}):(0,x.jsx)(`p`,{role:`status`,children:`正在读取入口画像…`}),N?.supported&&(0,x.jsxs)(x.Fragment,{children:[!!N.migration?.length&&(0,x.jsxs)(`details`,{className:`history`,children:[(0,x.jsx)(`summary`,{children:`旧画像迁移记录`}),(0,x.jsx)(`ul`,{children:N.migration.map(e=>(0,x.jsxs)(`li`,{children:[`旧版 v`,e.legacy_version,` · 原条目序号`,` `,e.sentence_index,`:`,{manual_imported:`已转为人工画像记忆`,manual_unclassified:`已转为管理员说明`,model_rebuild_pending:`原模型条目待按依据重建`,cleared_history:`原内容已清空`}[e.disposition]||e.disposition,e.memory_id!=null&&(0,x.jsxs)(`button`,{className:`text-button`,disabled:s,onClick:()=>te(e.memory_id),children:[`查看记忆 #`,e.memory_id]})]},`${e.legacy_version}:${e.sentence_index}`))})]}),(0,x.jsx)(Ar,{base:r,openMemory:te})]})]})})}var Ur={forget:`遗忘`,delete:`删除`,purge:`彻底清除`},Wr={memory_source:`仍是其他记忆的来源`,batch_segment:`等待或运行中的批次仍在使用`,goal_source:`仍是目标的来源`,subject_alias:`仍是主体别名的证据`,subject_link:`仍是主体联系的证据`,learning_request:`立即学习请求仍在使用`};function Gr({scope:e,name:t,onClose:n,onChanged:r}){let[i,a]=(0,l.useState)(`forget`),[o,s]=(0,l.useState)(!1),[c,u]=(0,l.useState)(null),[d,p]=(0,l.useState)(null),[m,h]=(0,l.useState)(!1),[y,b]=(0,l.useState)(``),[S,C]=(0,l.useState)(!1),[w,T]=(0,l.useState)(!1),D=d?.status===`completed`,O=()=>{u(null),b(``),C(!1),T(!1)};async function k(){h(!0),O();try{u(await g(`/memories/bulk/preview`,_(`POST`,{...e,action:i,include_pinned:o})))}catch(e){b(v(e))}finally{h(!1)}}async function j(){if(c&&!m&&c.applicable_count&&(i!==`purge`||w)){h(!0),b(``);try{p(await g(`/memories/bulk/apply`,_(`POST`,{snapshot_token:c.snapshot_token,action:i,confirm:i===`purge`&&w}))),u(null),C(!1),r()}catch(e){e instanceof f&&e.status===409?(O(),e.result&&(p(e.result),r()),b(e.message)):b(`${v(e)}。执行结果尚未确认,可使用当前预览重试;若服务已重启,请先查看操作记录。`)}finally{h(!1)}}}return(0,x.jsx)(ee,{title:`批量遗忘/删除`,onClose:n,closeDisabled:m,children:(0,x.jsxs)(`div`,{className:`lifecycle-stack bulk-memory`,children:[(0,x.jsxs)(`p`,{children:[(0,x.jsx)(`strong`,{children:t}),` ·`,` `,`subject_id`in e?`人物 ${e.subject_id}`:`入口 ${e.entry_id}`]}),(0,x.jsx)(`p`,{className:`muted`,children:`subject_id`in e?`范围:说话人或涉及人物为此人的记忆。`:`范围:至少一条直接来源消息属于此入口的记忆。`}),y&&(0,x.jsx)(A,{error:!0,children:y}),d&&(0,x.jsxs)(`section`,{"aria-label":`批量操作结果`,children:[(0,x.jsx)(A,{error:!D,children:D?`已批量${Ur[i]} ${d.count} 条记忆。`:`操作未全部完成,已处理 ${d.count} 条记忆。`}),(0,x.jsx)(ln,{notice:d.backup_notice}),d.skipped_count>0&&(0,x.jsxs)(`p`,{children:[`跳过已删除的 `,d.skipped_count,` 条记忆。`]}),(0,x.jsxs)(`details`,{children:[(0,x.jsxs)(`summary`,{children:[`已处理记忆 ID(`,d.memory_ids.length,` 条)`]}),(0,x.jsx)(`p`,{className:`bulk-ids`,children:d.memory_ids.map(e=>`#${e}`).join(`、`)||`无`})]}),i===`purge`&&(0,x.jsxs)(x.Fragment,{children:[(0,x.jsxs)(`p`,{children:[`已删除来源消息 `,d.deleted_message_ids.length,` 条;保留`,` `,d.retained_messages.length,` 条。`]}),(0,x.jsxs)(`details`,{children:[(0,x.jsx)(`summary`,{children:`来源消息处理明细`}),(0,x.jsxs)(`p`,{className:`bulk-ids`,children:[`已删除:`,d.deleted_message_ids.map(e=>`#${e}`).join(`、`)||`无`]}),d.retained_messages.map(e=>(0,x.jsxs)(`p`,{children:[`消息 #`,e.message_id,`:`,e.reasons.map(e=>Wr[e]||e).join(`;`)]},e.message_id))]})]})]}),!D&&(0,x.jsxs)(x.Fragment,{children:[!S&&(0,x.jsxs)(`fieldset`,{disabled:m,className:`bulk-options`,children:[(0,x.jsxs)(`label`,{children:[`目标操作`,(0,x.jsxs)(`select`,{value:i,onChange:e=>{a(e.target.value),O()},children:[(0,x.jsx)(`option`,{value:`forget`,children:`遗忘(可恢复)`}),(0,x.jsx)(`option`,{value:`delete`,children:`删除(撤销对象)`}),(0,x.jsx)(`option`,{value:`purge`,children:`彻底清除(不可撤销)`})]})]}),(0,x.jsxs)(`label`,{className:`check-label`,children:[(0,x.jsx)(`input`,{type:`checkbox`,checked:o,onChange:e=>{s(e.target.checked),O()}}),`包含置顶记忆`]}),(0,x.jsx)(`p`,{children:i===`forget`?`遗忘会降低保留强度并取消置顶;仍可深度召回及恢复。`:i===`delete`?`删除会撤销记忆对象,保留来源与历史;普通和深度召回均不再返回。`:`彻底清除会清除正文、修订历史,以及没有其他受保护引用的来源消息。`}),(0,x.jsx)(`button`,{className:`secondary`,onClick:()=>void k(),children:m?`正在处理…`:`预览范围`})]}),c&&(0,x.jsxs)(`section`,{"aria-label":`批量操作预览`,children:[(0,x.jsxs)(`h3`,{children:[`范围内 `,c.total,` 条,本次处理 `,c.applicable_count,` `,`条`]}),(0,x.jsxs)(`p`,{children:[`有效 `,c.counts.active,` · 遗忘 `,c.counts.forgotten,` `,`· 已删除 `,c.counts.deleted]}),(0,x.jsxs)(`p`,{children:[`包含置顶 `,c.pinned_count,` 条;排除置顶`,` `,c.excluded_pinned_count,` 条。`]}),(0,x.jsx)(`p`,{className:`muted`,children:`已删除对象只参加彻底清除。预览有效期 15 分钟,记忆变化后需重新预览。`}),(0,x.jsx)(`ul`,{className:`bulk-examples`,children:c.examples.map(e=>(0,x.jsxs)(`li`,{children:[(0,x.jsxs)(`span`,{className:`muted`,children:[`#`,e.id,` · `,E(e.lifecycle),e.pinned?` · 置顶`:``]}),(0,x.jsx)(`p`,{children:e.content})]},e.id))}),i===`purge`&&(0,x.jsx)(ln,{notice:c.backup_notice}),S?(0,x.jsxs)(x.Fragment,{children:[(0,x.jsxs)(A,{error:!0,children:[`即将彻底清除 `,c.applicable_count,` `,`条记忆,此操作不可撤销。同时清空可关联的模型原始输出等正文副本。`]}),(0,x.jsxs)(`label`,{className:`check-label`,children:[(0,x.jsx)(`input`,{type:`checkbox`,disabled:m,checked:w,onChange:e=>T(e.target.checked)}),`我理解此操作不可撤销`]}),(0,x.jsxs)(`div`,{className:`button-row`,children:[(0,x.jsx)(`button`,{className:`danger-button`,disabled:m||!w,onClick:()=>void j(),children:`再次确认并彻底清除`}),(0,x.jsx)(`button`,{className:`secondary`,disabled:m,onClick:()=>{C(!1),T(!1)},children:`返回预览`})]})]}):(0,x.jsxs)(`button`,{className:`danger-button`,disabled:m||!c.applicable_count,onClick:()=>i===`purge`?C(!0):void j(),children:[`确认批量`,Ur[i]]})]})]}),m&&(0,x.jsx)(`p`,{role:`status`,children:`正在处理,请等待结果…`}),!m&&(0,x.jsx)(`a`,{href:`#/operations?object_type=memory_bulk`,onClick:n,children:`查看批量操作记录`}),D&&(0,x.jsx)(`button`,{className:`primary`,onClick:n,children:`完成`})]})})}var Kr=e=>({group:`群聊`,private:`私聊`,live:`直播`})[e]||e,qr=e=>({message:`消息`,self_output:`角色实际输出`,action_result:`行动结果`,event:`场景事件`})[e]||e,Jr=e=>({attempts_exhausted:`尝试用尽,未学习`,content_rejection:`内容拒绝,未学习`,interrupted:`学习中断`})[e]||e;function Yr(e){let t=`已自动重试 ${e.attempted}/${e.total}`;return{scheduled:`${t},下次 ${e.next_date}`,in_progress:e.attempted?t:``,finished:`${t},不再自动重试,可手动重新学习`,disabled:`${t},自动重试已关闭`,targets_cleared:`${t},目标消息已清理,不再自动重试`,refused:`内容拒绝,不自动重试,可手动重新学习`}[e.status]}var Xr=e=>typeof e==`string`?e:JSON.stringify(e,null,2);function Zr({openMemory:e}){let[t,n]=(0,l.useState)(`entries`),[r,i]=(0,l.useState)(null),[a,o]=(0,l.useState)(null),[s,c]=(0,l.useState)(null),[u,d]=(0,l.useState)(null),[f,p]=(0,l.useState)(``),[m,h]=(0,l.useState)({entry_id:``,time_from:``,time_to:``}),[g,_]=(0,l.useState)(m),[v,b]=(0,l.useState)(0),C=y(`/entries`,5e3),w=new URLSearchParams({limit:`30`,offset:String(v)});for(let[e,t]of Object.entries(g))t&&w.set(e,t);let T=y(t===`batches`&&r===null?`/batches?${w}`:null,3e3),E=y(t===`gaps`&&r===null?`/memory-gaps?${w}`:null,3e3),D=t===`batches`?T:E,O=()=>{C.refresh(),T.refresh(),E.refresh()};function k(e){n(e),i(null),b(0)}function ee(e){let t={entry_id:e,time_from:``,time_to:``};h(t),_(t),k(`batches`)}return(0,x.jsxs)(x.Fragment,{children:[(0,x.jsx)(`div`,{className:`page-heading`,children:(0,x.jsxs)(`div`,{children:[(0,x.jsx)(`p`,{className:`eyebrow`,children:`从消息到记忆`}),(0,x.jsx)(`h1`,{children:`入口与学习`}),(0,x.jsx)(`p`,{children:`查看每个入口的学习进度,追溯批次结果与尚未记住的消息。`})]})}),(0,x.jsx)(`div`,{className:`learning-tabs`,role:`group`,"aria-label":`入口与学习视图`,children:[[`entries`,`入口列表`],[`batches`,`学习批次`],[`gaps`,`记忆缺口`]].map(([e,n])=>(0,x.jsx)(`button`,{"aria-pressed":t===e,onClick:()=>k(e),children:n},e))}),C.error&&(0,x.jsxs)(A,{error:!0,children:[C.error,(0,x.jsx)(`button`,{onClick:C.refresh,children:`重试`})]}),f&&(0,x.jsx)(A,{children:f}),a&&(0,x.jsx)(Gr,{scope:{entry_id:a.id},name:a.name,onClose:()=>o(null),onChanged:O}),u&&(0,x.jsx)(Hr,{entry:u,onClose:()=>d(null),openMemory:e},u.id),s&&(0,x.jsx)(ct,{entry:s,entries:C.data?.items||[],onClose:()=>c(null),onSaved:()=>{c(null),p(`入口设置已保存`),O()}}),r===null?t===`entries`?(0,x.jsxs)(x.Fragment,{children:[(0,x.jsx)(`p`,{className:`learning-intro`,children:`已接收的消息可能仍在等待学习;批次结束后,也可能没有形成记忆。`}),C.data?C.data.items.length?(0,x.jsx)(`div`,{className:`entry-grid`,children:C.data.items.map(e=>(0,x.jsxs)(`article`,{className:`panel learning-entry`,children:[(0,x.jsxs)(`div`,{className:`section-title`,children:[(0,x.jsx)(`h2`,{children:e.name}),(0,x.jsx)(M,{children:Kr(e.kind)})]}),(0,x.jsx)(`p`,{className:`muted`,children:e.platform===`iris-trial`?`试用入口`:`宿主入口 · ${e.platform}`}),(0,x.jsxs)(`div`,{className:`learning-entry-meta`,children:[(0,x.jsx)(M,{children:at(e.pace)}),(0,x.jsxs)(`strong`,{children:[`待学习 `,e.pending_count,` 条`]})]}),(0,x.jsxs)(`p`,{className:`entry-filter-summary`,children:[`记忆可见范围:`,tt(e,C.data?.items||[])]}),(0,x.jsx)(ot,{value:e.filters}),(0,x.jsx)(st,{value:e.queue_wait}),(0,x.jsxs)(`p`,{children:[`最近批次`,e.latest_batch?` #${e.latest_batch.id}`:``]}),(0,x.jsx)(`p`,{children:e.latest_batch?lr(e.latest_batch):`暂无批次`}),e.current_batch&&e.current_batch.id!==e.latest_batch?.id&&(0,x.jsxs)(`p`,{children:[`当前处理 #`,e.current_batch.id,` ·`,` `,lr(e.current_batch)]}),(0,x.jsx)(`button`,{className:`secondary`,"aria-label":`修改入口设置 · ${e.name}`,onClick:()=>{p(``),c(e)},children:`修改入口设置`}),(0,x.jsx)(`button`,{className:`secondary`,"aria-label":`入口画像 · ${e.name}`,onClick:()=>d(e),children:`入口画像`}),(0,x.jsx)(`button`,{className:`secondary`,"aria-label":`批量遗忘/删除 · ${e.name}`,onClick:()=>o(e),children:`批量遗忘/删除`}),(0,x.jsx)(`button`,{className:`text-button`,"aria-label":`查看批次 · ${e.name}`,onClick:()=>ee(e.id),children:`查看批次 →`})]},e.id))}):(0,x.jsx)(j,{title:`还没有入口`,children:`可在试用对话页创建入口,或由宿主发送消息。`}):(0,x.jsx)(`p`,{children:`正在读取入口…`})]}):(0,x.jsxs)(x.Fragment,{children:[(0,x.jsx)(`p`,{className:`learning-intro`,children:t===`gaps`?`放弃或内容拒绝的批次留下记忆缺口。重新学习成功后才会消失;模型暂停期间的普通积压不算缺口。`:`批次保留冻结的三段消息。成功可以形成零条记忆;失败重试和放弃、拒绝会分别显示。`}),(0,x.jsxs)(`form`,{className:`learning-filters`,onSubmit:e=>{e.preventDefault(),_({...m}),b(0)},children:[(0,x.jsxs)(`label`,{children:[`筛选入口`,(0,x.jsxs)(`select`,{value:m.entry_id,onChange:e=>h({...m,entry_id:e.target.value}),children:[(0,x.jsx)(`option`,{value:``,children:`全部入口`}),C.data?.items.map(e=>(0,x.jsx)(`option`,{value:e.id,children:e.name},e.id))]})]}),(0,x.jsxs)(`label`,{children:[`开始日期`,(0,x.jsx)(`input`,{type:`date`,value:m.time_from,onChange:e=>h({...m,time_from:e.target.value})})]}),(0,x.jsxs)(`label`,{children:[`结束日期`,(0,x.jsx)(`input`,{type:`date`,value:m.time_to,min:m.time_from||void 0,onChange:e=>h({...m,time_to:e.target.value})})]}),(0,x.jsx)(`button`,{className:`secondary`,type:`submit`,children:`筛选`})]}),(0,x.jsxs)(`p`,{className:`muted learning-date-help`,children:[`日期按角色时区;`,t===`gaps`?`显示与日期范围重叠的消息区间。`:`按批次组成时间筛选。`]}),D.error&&(0,x.jsxs)(A,{error:!0,children:[D.error,(0,x.jsx)(`button`,{onClick:D.refresh,children:`重试`})]}),t===`batches`&&T.data?.entry_waits?.filter(e=>e.queue_wait.reason).map(e=>(0,x.jsxs)(`div`,{className:`panel`,children:[(0,x.jsxs)(`strong`,{children:[C.data?.items.find(t=>t.id===e.entry_id)?.name||e.entry_id,` `,`· 当前入口等待`]}),(0,x.jsx)(st,{value:e.queue_wait})]},e.entry_id)),D.data?(0,x.jsxs)(x.Fragment,{children:[D.data.total===0?(0,x.jsx)(j,{title:t===`gaps`?`当前范围没有记忆缺口`:`当前范围没有批次`}):(0,x.jsx)(`div`,{className:`learning-list`,children:t===`batches`?T.data?.items.map(e=>(0,x.jsxs)(`article`,{className:`panel learning-batch-card`,children:[(0,x.jsxs)(`div`,{className:`learning-row`,children:[(0,x.jsxs)(`div`,{children:[(0,x.jsxs)(`strong`,{children:[`#`,e.id,` · `,e.entry_name]}),(0,x.jsx)(`p`,{children:lr(e)}),(0,x.jsxs)(`small`,{children:[S(e.created_at),` ·`,` `,sr(e)?`拆分前`:`本轮`,`已计`,` `,e.attempt_count,` 次尝试`]})]}),(0,x.jsx)(`button`,{className:`text-button`,"aria-label":`查看批次 #${e.id}`,onClick:()=>i(e.id),children:`查看详情 →`})]}),(0,x.jsx)(fr,{batch:e,onOpen:i}),sr(e)&&(0,x.jsxs)(`div`,{className:`learning-batch-tree`,children:[(0,x.jsx)(dr,{batch:e}),(0,x.jsx)(vr,{id:e.id,onOpen:i})]})]},e.id)):E.data?.items.map(e=>(0,x.jsxs)(`article`,{className:`panel learning-row`,children:[(0,x.jsxs)(`div`,{children:[(0,x.jsxs)(`strong`,{children:[e.entry_name,` · 批次 #`,e.batch_id]}),(0,x.jsxs)(`p`,{children:[S(e.started_at),` — `,S(e.ended_at)]}),(0,x.jsx)(`p`,{children:Jr(e.reason)}),Yr(e.auto_retry)&&(0,x.jsx)(`p`,{children:Yr(e.auto_retry)}),e.relearning&&(0,x.jsx)(M,{children:`重新学习中`})]}),(0,x.jsx)(`button`,{className:`text-button`,"aria-label":`查看批次 #${e.batch_id}`,onClick:()=>i(e.batch_id),children:`查看详情 →`})]},e.batch_id))}),(0,x.jsxs)(`div`,{className:`learning-pagination`,children:[(0,x.jsxs)(`span`,{children:[`共 `,D.data.total,` `,t===`batches`?`个批次节点(含子批次)`:`处缺口`]}),(0,x.jsx)(`button`,{disabled:v===0,onClick:()=>b(Math.max(0,v-30)),children:`上一页`}),(0,x.jsx)(`button`,{disabled:v+30>=D.data.total,onClick:()=>b(v+30),children:`下一页`})]})]}):(0,x.jsx)(`p`,{children:`正在读取…`})]}):(0,x.jsx)(ei,{id:r,openMemory:e,onBack:()=>i(null),onRelearn:O,onOpen:i},r)]})}function Qr({calls:e}){return e.length?(0,x.jsx)(`div`,{className:`learning-calls`,children:e.map(e=>(0,x.jsxs)(`div`,{children:[(0,x.jsxs)(`strong`,{children:[e.purpose===`learning_repair`?`JSON 修正请求`:`学习请求`,` `,`· `,e.model]}),(0,x.jsxs)(`dl`,{children:[(0,x.jsxs)(`div`,{children:[(0,x.jsx)(`dt`,{children:`请求结果`}),(0,x.jsxs)(`dd`,{children:[e.result_category===`success`?`已返回正文`:e.result_category,e.timed_out?` · 超时`:``]})]}),(0,x.jsxs)(`div`,{children:[(0,x.jsx)(`dt`,{children:`耗时`}),(0,x.jsx)(`dd`,{children:C(e.duration_ms)})]}),(0,x.jsxs)(`div`,{children:[(0,x.jsx)(`dt`,{children:`结束原因(finish_reason)`}),(0,x.jsx)(`dd`,{children:e.finish_reason||`未提供`})]}),(0,x.jsxs)(`div`,{children:[(0,x.jsx)(`dt`,{children:`推理档位`}),(0,x.jsx)(`dd`,{children:e.reasoning_effort||`未记录 / 服务商默认`})]})]}),e.error_summary&&(0,x.jsx)(`p`,{children:e.error_summary})]},e.id))}):(0,x.jsx)(`p`,{className:`muted`,children:`没有可关联的调用记录;结束原因与推理档位未知。`})}function $r({label:e,value:t}){return t==null?(0,x.jsxs)(`p`,{className:`muted`,children:[e,`:没有记录`]}):(0,x.jsxs)(`details`,{className:`learning-output`,children:[(0,x.jsx)(`summary`,{children:e}),(0,x.jsx)(`pre`,{children:t||`(空正文)`})]})}function ei({id:e,onBack:t,openMemory:n,onRelearn:r,onOpen:i}){let a=y(`/batches/${e}`,3e3),[o,s]=(0,l.useState)(!1),[c,u]=(0,l.useState)(``),[d,f]=(0,l.useState)(``),p=a.data;async function m(){if(!o&&p?.can_relearn&&!sr(p)&&[`abandoned`,`refused`].includes(p.state)){s(!0),f(``),u(``);try{await g(`/batches/${e}/relearn`,_(`POST`,{})),u(`已重新排队,尚未记住。模型恢复且本入口在途批次结束后开始学习。`),r()}catch(e){f(v(e))}finally{s(!1),a.refresh()}}}return(0,x.jsxs)(`section`,{"aria-label":`批次 #${e}`,className:`batch-detail`,children:[(0,x.jsx)(`button`,{className:`text-button`,onClick:t,children:`← 返回列表`}),(a.error||d)&&(0,x.jsx)(A,{error:!0,children:d||a.error}),c&&(0,x.jsx)(A,{children:c}),p?(0,x.jsxs)(x.Fragment,{children:[(0,x.jsxs)(`div`,{className:`panel batch-overview`,children:[(0,x.jsxs)(`div`,{className:`section-title`,children:[(0,x.jsxs)(`h2`,{children:[`批次 #`,e,` · `,p.entry.name]}),(0,x.jsx)(M,{children:lr(p,p.segments.target.length)})]}),(0,x.jsx)(fr,{batch:p,onOpen:i}),(0,x.jsxs)(`p`,{className:`muted`,children:[`组成于 `,S(p.created_at),p.finished_at?` · 结束于 ${S(p.finished_at)}`:``,` `,`· `,sr(p)?`拆分前`:`本轮`,`已计`,` `,p.attempt_count,` / 4 次尝试`]}),p.parent_batch_id&&(0,x.jsx)(mr,{id:e}),(0,x.jsx)(hr,{batch:p}),sr(p)&&(0,x.jsx)(dr,{batch:p}),!sr(p)&&p.gap&&(0,x.jsx)(`p`,{children:`仍有记忆缺口;成功学习后才会消失。`}),p.last_error&&(0,x.jsxs)(`p`,{children:[sr(p)?`子批次最近错误`:`最近错误`,`:`,p.last_error]}),p.relearn_blocked_reason&&!sr(p)&&(0,x.jsx)(`p`,{children:p.relearn_blocked_reason}),p.can_relearn&&!sr(p)&&[`abandoned`,`refused`].includes(p.state)&&(0,x.jsxs)(`div`,{className:`relearn-action`,children:[(0,x.jsx)(`button`,{className:`primary`,disabled:o,onClick:m,children:o?`正在提交…`:`重新学习`}),(0,x.jsx)(`p`,{children:`保留原批次范围和历史记录,重新获得 4 次尝试;成功前缺口持续存在。`})]}),p.state===`succeeded`&&!p.memories.length&&(0,x.jsx)(`p`,{children:`本批已学习,形成或更新了 0 条记忆。这是正常的成功结果。`}),!sr(p)&&p.state!==`succeeded`&&(0,x.jsx)(`p`,{children:`尚未成功学习,不能把这些消息当作已经记住。`})]}),sr(p)&&(0,x.jsxs)(`section`,{className:`panel`,children:[(0,x.jsx)(`h2`,{children:`子批次结果`}),(0,x.jsx)(`p`,{children:`本节点的历史请求曾被拒绝,已拆分交给子批次。请进入失败子批次详情重新学习。`}),(0,x.jsx)(vr,{id:e,onOpen:i})]}),(0,x.jsxs)(`section`,{children:[(0,x.jsx)(`h2`,{children:sr(p)?`拆分前冻结的三段消息`:`三段消息`}),(0,x.jsxs)(`details`,{className:`panel`,children:[(0,x.jsx)(`summary`,{children:`组成批次时的入口设置`}),p.entry_settings?(0,x.jsxs)(x.Fragment,{children:[(0,x.jsxs)(`p`,{children:[`学习节奏:`,at(p.entry_settings.pace)]}),(0,x.jsx)(ot,{value:p.entry_settings.filters})]}):(0,x.jsx)(`p`,{children:`该批次没有保存入口设置快照。`})]}),(0,x.jsx)(`p`,{className:`muted`,children:`三段范围已冻结;历史与后续只帮助理解,只有目标段可以形成记忆。这里保留消息原文。`}),p.context_mode===`target_only`&&(0,x.jsx)(`p`,{className:`notice`,children:`当前仅目标模式:下方历史段与后续段保留供追溯,不会作为可选前后文发送;仍保留目标引用、附件及既有相关记忆等材料。`}),(0,x.jsx)(`div`,{className:`batch-segments`,children:[[`history`,`历史段`,`只帮助理解`],[`target`,`目标段`,`只从这里学习`],[`future`,`后续段`,`以后再学习`]].map(([e,t,n])=>(0,x.jsxs)(`section`,{className:`panel segment segment-${e}`,children:[(0,x.jsxs)(`div`,{className:`section-title`,children:[(0,x.jsxs)(`h3`,{children:[t,` · `,p.segments[e].length,` 条`]}),(0,x.jsx)(`small`,{children:n})]}),!p.segments[e].length&&(0,x.jsx)(`p`,{className:`muted`,children:`本段为空`}),p.segments[e].map(e=>(0,x.jsx)(`article`,{className:`batch-message`,children:e.missing?(0,x.jsxs)(`p`,{children:[`消息 #`,e.id,` 已不可用`]}):(0,x.jsxs)(x.Fragment,{children:[(0,x.jsxs)(`div`,{children:[(0,x.jsx)(`strong`,{children:e.sender_name||e.sender_subject_id}),` `,(0,x.jsx)(M,{children:qr(e.kind)})]}),(0,x.jsxs)(`small`,{children:[`#`,e.id,` · `,S(e.occurred_at),e.scene_identity?` · ${e.scene_identity}`:``]}),e.content.length>600?(0,x.jsxs)(`details`,{children:[(0,x.jsxs)(`summary`,{children:[e.content.slice(0,120),`…(展开原文)`]}),(0,x.jsx)(`p`,{className:`message-body`,children:e.content})]}):(0,x.jsx)(`p`,{className:`message-body`,children:e.content}),(0,x.jsx)(qe,{media:e.media}),e.quote_content&&(0,x.jsxs)(`blockquote`,{children:[e.quote_author_name||`引用`,`:`,e.quote_content]}),(0,x.jsx)(`small`,{children:O(e.learning_state)})]})},e.id))]},e))})]}),(0,x.jsxs)(`section`,{children:[(0,x.jsx)(`h2`,{children:sr(p)?`本节点历史尝试(拆分前)`:`每次尝试`}),sr(p)&&(0,x.jsx)(`p`,{children:`以下只记录本节点拆分前的请求;这些历史拒绝不等于子批次最终失败。`}),(0,x.jsxs)(`p`,{children:[`本节点实际模型调用 `,ur(p),` 次`]}),(0,x.jsx)(`p`,{className:`muted`,children:`含学习、JSON 修正和未归入尝试的调用;不包含子批次调用,不等于树节点数或尝试额度。`}),(0,x.jsx)(`p`,{className:`muted`,children:`历次尝试持续编号;服务暂停时的失败记录不一定扣除尝试额度。模型返回正文不代表已经写入记忆。`}),!p.attempts.length&&(0,x.jsx)(j,{title:p.state===`running`?`本次尝试仍在进行`:`尚无已完成的尝试`}),p.attempts.map(e=>(0,x.jsxs)(`article`,{className:`panel batch-attempt`,children:[(0,x.jsxs)(`div`,{className:`section-title`,children:[(0,x.jsxs)(`h3`,{children:[`第 `,e.number,` 次尝试`]}),(0,x.jsx)(M,{children:e.parse_status===`failed`?`失败 · 未写入`:`学习成功`})]}),(0,x.jsxs)(`p`,{children:[S(e.started_at),` — `,S(e.finished_at),` · 总耗时 `,C(e.duration_ms)]}),(0,x.jsxs)(`p`,{children:[`解析结果:`,{direct:`直接解析`,repaired:`修正后解析`,failed:`失败`}[e.parse_status]||e.parse_status]}),(0,x.jsxs)(`p`,{children:[`本次上下文模式:`,cr(e.context_mode)]}),e.error&&(0,x.jsxs)(`p`,{children:[`诊断:`,e.error]}),(0,x.jsx)(Qr,{calls:e.calls}),(0,x.jsx)($r,{label:`模型原始正文`,value:e.raw_output}),(0,x.jsx)($r,{label:`JSON 修正后的正文`,value:e.repair_output})]},e.id)),!!p.unassigned_calls.length&&(0,x.jsxs)(`details`,{className:`panel batch-attempt`,children:[(0,x.jsxs)(`summary`,{children:[`尚未归入已完成尝试的调用 · `,p.unassigned_calls.length,` 条`]}),(0,x.jsx)(Qr,{calls:p.unassigned_calls})]})]}),(0,x.jsxs)(`div`,{className:`batch-results`,children:[(0,x.jsxs)(`section`,{className:`panel`,children:[(0,x.jsxs)(`h2`,{children:[`规整记录 · `,p.result.normalizations?.length||0,` 条`]}),!p.result.normalizations?.length&&(0,x.jsx)(`p`,{className:`muted`,children:`没有规整记录。`}),p.result.normalizations?.map((e,t)=>(0,x.jsxs)(`article`,{className:`result-item`,children:[(0,x.jsx)(`strong`,{children:e.field}),(0,x.jsx)(`p`,{children:e.reason}),(0,x.jsxs)(`small`,{children:[e.section,e.index==null?``:` · 第 ${e.index+1} 项`]}),(0,x.jsxs)(`pre`,{children:[Xr(e.before),` → `,Xr(e.after)]})]},t))]}),(0,x.jsxs)(`section`,{className:`panel`,children:[(0,x.jsxs)(`h2`,{children:[`被丢弃的条目 · `,p.result.dropped?.length||0,` 条`]}),!p.result.dropped?.length&&(0,x.jsx)(`p`,{className:`muted`,children:`没有已记录的丢弃条目;批次失败时请查看原始输出和错误。`}),p.result.dropped?.map((e,t)=>(0,x.jsxs)(`article`,{className:`result-item`,children:[(0,x.jsx)(`strong`,{children:e.reason}),(0,x.jsx)(`small`,{children:e.section}),(0,x.jsx)(`pre`,{children:Xr(e.item)})]},t))]})]}),(0,x.jsxs)(`section`,{className:`panel`,children:[(0,x.jsxs)(`h2`,{children:[sr(p)?`成功子批次的记忆结果`:`记忆结果`,` ·`,` `,p.memories.length,` 条`]}),(0,x.jsx)(`p`,{className:`muted`,children:`下列正文为记忆当前版本;详情中可查看修订历史。`}),!p.memories.length&&(0,x.jsx)(`p`,{children:p.state===`succeeded`?`本批没有新增、更新或确认记忆。`:`尚无成功写入的记忆。`}),p.memories.map(e=>(0,x.jsxs)(`div`,{className:`learning-row`,children:[(0,x.jsx)(M,{children:{created:`新增`,updated:`更新`,confirmed:`再次确认`}[e.change]||e.change}),e.missing?(0,x.jsxs)(`span`,{children:[`记忆 #`,e.id,` 已不可用`]}):(0,x.jsxs)(`button`,{className:`text-button`,onClick:()=>n(e.id),children:[`#`,e.id,` · `,e.content]}),(0,x.jsxs)(`small`,{children:[E(e.lifecycle),` · 修订 `,e.revision]})]},`${e.change}-${e.id}`))]})]}):(0,x.jsx)(`p`,{children:`正在读取批次…`})]})}function ti(e){if(!e||typeof e!=`object`)return null;let t=e;return typeof t.reason!=`string`||!Array.isArray(t.items)||!t.items.every(e=>{if(!e||typeof e!=`object`)return!1;let t=e;return Number.isInteger(t.index)&&typeof t.supported==`boolean`&&typeof t.scene_qualified==`boolean`&&typeof t.reason==`string`&&Array.isArray(t.violations)&&t.violations.every(e=>typeof e==`string`)})?null:t}function ni(e){if(!e||typeof e!=`object`)return`原条目格式无效或未保存`;let t=e.text;return typeof t==`string`?t:`原条目格式无效或未保存`}function ri({value:e,openMemory:t}){return e.length?(0,x.jsxs)(A,{children:[(0,x.jsx)(`strong`,{children:`可能过时`}),(0,x.jsx)(`ul`,{children:e.map((e,n)=>(0,x.jsxs)(`li`,{children:[e.memory_id!=null&&(0,x.jsxs)(`button`,{className:`text-button`,onClick:()=>t(e.memory_id),children:[`记忆 #`,e.memory_id]}),e.revision!=null&&`(依据修订 ${e.revision})`,e.sentence_index!=null&&`第 ${e.sentence_index+1} 条:`,Ut(e.reason||``)]},n))})]}):null}function ii({item:e,openMemory:t}){return(0,x.jsxs)(`div`,{className:`summary-item`,children:[(0,x.jsxs)(`p`,{children:[e.kind&&(0,x.jsx)(M,{children:Vt[e.kind]||e.kind}),` `,(0,x.jsx)(M,{children:e.admin_written||e.author===`admin`?`管理员手写`:`模型生成`})]}),(0,x.jsx)(`p`,{className:`entry-profile-content`,children:e.body||e.text}),e.time_range&&(0,x.jsxs)(`p`,{children:[`时间范围:`,e.time_range]}),e.valid_until&&(0,x.jsxs)(`p`,{children:[`有效至:`,e.valid_until]}),e.expires_at&&(0,x.jsxs)(`p`,{children:[`到期时间:`,w(e.expires_at)]}),(0,x.jsxs)(`details`,{className:`source`,children:[(0,x.jsxs)(`summary`,{children:[`记忆依据 · `,e.basis.length,` 条`]}),!e.basis.length&&(0,x.jsx)(`p`,{children:`此条没有模型记忆依据;管理员手写内容由管理员负责核对。`}),e.basis.map((e,n)=>(0,x.jsxs)(`div`,{className:`entry-profile-evidence`,children:[(0,x.jsxs)(`button`,{className:`text-button`,onClick:()=>t(e.memory_id),children:[`记忆 #`,e.memory_id]}),` `,`· 依据修订 `,e.revision,e.evidence?(0,x.jsxs)(x.Fragment,{children:[(0,x.jsx)(`p`,{className:`entry-profile-content`,children:e.evidence.content}),(0,x.jsxs)(`small`,{children:[`依据快照 ·`,` `,w(e.evidence.event_time||e.evidence.created_at),e.evidence.truncated&&` · 摘录已截短`]})]}):(0,x.jsx)(`p`,{className:`muted`,children:`没有保存的依据正文快照。`}),e.memory?(0,x.jsxs)(`details`,{children:[(0,x.jsxs)(`summary`,{children:[`当前记忆 · 修订 `,e.memory.revision]}),(0,x.jsx)(`p`,{children:e.memory.content})]}):(0,x.jsx)(`p`,{className:`muted`,children:`当前记忆已不可用。`})]},n))]})]})}function ai({value:e}){return(0,x.jsxs)(`p`,{className:`muted`,children:[`v`,e.version,` · `,Ht[e.status]||e.status,` ·`,` `,{periodic:`定期更新`,regenerate:`管理员重新生成`,edit:`管理员编辑`,rollback:`回滚`}[e.source]||e.source,` `,`· 生成于 `,w(e.generated_at),` · 变化程度:`,{small:`小`,medium:`中`,large:`大`}[e.change_degree]||e.change_degree]})}function oi({value:e,openMemory:t}){let n=e.checks,r=ti(n.model);return e.cleared_at?(0,x.jsxs)(x.Fragment,{children:[(0,x.jsx)(ai,{value:e}),(0,x.jsxs)(A,{children:[`因彻底清除而清空 · `,w(e.cleared_at),`。此版本不能回滚。`]})]}):(0,x.jsxs)(x.Fragment,{children:[(0,x.jsx)(ai,{value:e}),e.invalidated_reason&&(0,x.jsxs)(A,{children:[Ut(e.invalidated_reason),`,此版本不可回滚。`]}),(0,x.jsx)(ri,{value:e.stale_basis,openMemory:t}),e.sentences.map((e,n)=>(0,x.jsx)(ii,{item:e,openMemory:t},n)),!e.sentences.length&&(0,x.jsx)(`p`,{children:`此版本没有摘要条目。`}),(0,x.jsxs)(`details`,{className:`source`,children:[(0,x.jsx)(`summary`,{children:`检查与删条记录`}),(0,x.jsxs)(`p`,{children:[`检查结果:`,n.passed?`通过`:`未通过`,n.admin_edit&&`(管理员编辑,未调用模型检查)`]}),n.reason&&(0,x.jsx)(`p`,{children:Ut(n.reason)}),n.errors?.map((e,t)=>(0,x.jsx)(`p`,{children:Ut(e)},t)),n.deterministic&&(0,x.jsxs)(`p`,{children:[`规则检查:`,n.deterministic.passed?`通过`:`未通过`,n.deterministic.characters!=null&&` · ${n.deterministic.characters} 字`,n.deterministic.tokens!=null&&` · ${n.deterministic.tokens} token`]}),r?(0,x.jsxs)(x.Fragment,{children:[(0,x.jsxs)(`p`,{children:[`模型检查:`,r.reason]}),r.items.map((e,t)=>(0,x.jsxs)(`div`,{className:`entry-profile-evidence`,children:[(0,x.jsxs)(`strong`,{children:[`检查条目 `,e.index]}),(0,x.jsx)(`p`,{children:n.checked_sentences?.[t]?.text}),(0,x.jsxs)(`p`,{children:[`依据支持:`,e.supported?`是`:`否`,` · 场景限定:`,e.scene_qualified?`充分`:`不足`]}),(0,x.jsx)(`p`,{children:e.reason}),!!e.violations.length&&(0,x.jsxs)(`p`,{children:[`问题:`,e.violations.join(`、`)]})]},e.index))]}):(0,x.jsx)(`p`,{children:n.model==null?`没有模型检查记录。`:`模型检查记录格式无效,无法逐条展示。`}),!!n.deleted_sentences?.length&&(0,x.jsxs)(x.Fragment,{children:[(0,x.jsx)(`h4`,{children:`已删条目`}),n.deleted_sentences.map((e,t)=>(0,x.jsxs)(`div`,{children:[(0,x.jsxs)(`p`,{children:[`第 `,e.index,` 条 ·`,` `,e.stage===`references`?`引用核对`:`模型检查`]}),(0,x.jsx)(`p`,{children:e.sentence?.text||ni(e.item)}),(0,x.jsx)(`p`,{children:Ut(e.reason)}),!!e.violations?.length&&(0,x.jsx)(`p`,{children:e.violations.join(`、`)})]},t))]}),n.admin_warnings?.map((e,t)=>(0,x.jsxs)(A,{children:[`手写条目 `,e.index,` 的检查提醒:`,e.reason,` ·`,` `,e.violations.join(`、`)]},t)),n.rollback_from!=null&&(0,x.jsxs)(`p`,{children:[`从 v`,n.rollback_from,` 回滚新建。`]})]})]})}function si(e){let t=Array.from(e),n=0,r=0,i=0;for(;ie.kind).map(e=>e.text))].sort((e,t)=>t.length-e.length),r=0,i=0;if(n.length){let t=RegExp(`^(?:`+n.map(e=>e.replace(/[.*+?^${}()|[\]\\]/g,`\\$&`)).join(`|`)+`)`,`gm`);for(let n of e.matchAll(t))r+=si(e.slice(i,n.index))+1,i=n.index+n[0].length}r+=si(e.slice(i));let a=Array.from(e),o=a.filter(e=>/[\u4e00-\u9fff]/u.test(e)).length;return{items:r,characters:a.length,tokens:Math.max(1,o+Math.floor((a.length-o+2)/3))}}function li({base:e,initial:t,onSettled:n,view:r}){let[i,a]=(0,l.useState)(t.state===`running`),o=y(`${e}/attempts/${t.id}`,i?1500:0),s=(0,l.useRef)(!1);(0,l.useEffect)(()=>{o.data&&o.data.state!==`running`&&!s.current&&(s.current=!0,a(!1),n())},[o.data,n]);let c=o.data||t;return(0,x.jsxs)(`section`,{className:`entry-profile-section`,"aria-label":`摘要生成任务`,children:[(0,x.jsxs)(`h3`,{children:[`生成任务 #`,c.id]}),o.error&&(0,x.jsxs)(A,{error:!0,children:[o.error,(0,x.jsx)(`button`,{onClick:o.refresh,children:`重试任务状态`})]}),(0,x.jsx)(`p`,{role:`status`,children:c.state===`running`?c.stage===`checking`?`正在检查候选…`:`正在生成候选…`:Ht[c.state]||c.state}),c.reason&&(0,x.jsx)(`p`,{children:Ut(c.reason)}),(0,x.jsxs)(`p`,{className:`fine-print`,children:[`开始:`,w(c.started_at),c.finished_at&&` · 结束:${w(c.finished_at)}`]}),c.state===`rejected`&&(0,x.jsx)(`p`,{children:`当前发布版保持不变。确定性拒绝后自动整理等待新依据;管理员仍可主动重新生成。`}),c.cleared_at&&(0,x.jsx)(`p`,{children:`任务关联内容因彻底清除而清空。`}),c.version!=null&&(0,x.jsxs)(`button`,{onClick:()=>r(c.version),children:[`查看任务版本 v`,c.version]})]})}function ui({subjectId:e,initialVersion:t,openMemory:n}){let r=`/people/${encodeURIComponent(e)}/summary`,i=y(r,5e3),[a,o]=(0,l.useState)(0),[s,c]=(0,l.useState)(t||null),u=y(`${r}/versions?limit=10&offset=${a}`),d=y(i.data?.current?`${r}/versions/${i.data.current.version}`:null),p=y(s?`${r}/versions/${s}`:null),[h,g]=(0,l.useState)(null),b=y(h),[S,C]=(0,l.useState)(`1`),[E,D]=(0,l.useState)(`2`),[O,k]=(0,l.useState)(null),[j,N]=(0,l.useState)(null),[P,F]=(0,l.useState)(null),[I,te]=(0,l.useState)(``),[L,ne]=(0,l.useState)(null),[R,re]=(0,l.useState)(!1),[ie,ae]=(0,l.useState)(``),[oe,se]=(0,l.useState)(``),[ce,le]=(0,l.useState)(!1),ue=(0,l.useRef)(!1),de=(0,l.useRef)(!0);(0,l.useEffect)(()=>(de.current=!0,()=>{de.current=!1}),[]);let z=(0,l.useCallback)(()=>{i.refresh(),u.refresh(),d.refresh(),p.refresh(),b.refresh()},[i.refresh,u.refresh,d.refresh,p.refresh,b.refresh]),B=i.data,V=B?.settings.current_version??0,fe=JSON.stringify(B?.stale_basis||[]);(0,l.useEffect)(()=>{u.refresh(),d.refresh(),p.refresh()},[V,B?.current?.cleared_at,fe,B?.pending?.version,B?.latest_attempt?.state,u.refresh,d.refresh,p.refresh]);let pe=e=>{c(e),ae(``)},me=B?.latest_attempt,he=me&&(!L||me.id>=L.id)?me:L,H=he?.state===`running`,U=R||i.loading||!!i.error||ce,ge=!B?.current||d.data?.version===V&&!d.loading&&!d.error,_e=ci(O?.content||``,O?.original),ve=B?.edit_limits||{max_items:5,max_chars:200,max_tokens:250},ye=!O?.content.trim()||_e.characters>ve.max_chars||_e.tokens>ve.max_tokens||_e.items>ve.max_items;async function be(e,t,n,i){if(ue.current)return!1;ue.current=!0,re(!0),ae(``),se(``);try{let a=await m(r+e,_(t,n));return de.current&&(e===`/regenerate`&&a.status===202&&a.data.attempt&&ne(a.data.attempt),se(i),F(null),z()),!0}catch(e){if(de.current){let t=e instanceof f&&e.status===409;le(t),ae(t?`当前摘要、设置、依据或生成任务已变化。已刷新版本;请核对后再操作,编辑草稿已保留。`:v(e)),t&&F(null),z()}return!1}finally{ue.current=!1,de.current&&re(!1)}}let xe=(e,t)=>{C(String(e)),D(String(t)),g(`${r}/diff?before_version=${e}&after_version=${t}`)};return(0,x.jsxs)(`section`,{className:`panel person-summary`,"aria-label":`人物摘要`,children:[(0,x.jsxs)(`div`,{className:`panel-heading`,children:[(0,x.jsx)(`h2`,{children:`人物摘要`}),(0,x.jsx)(`button`,{disabled:R,onClick:z,children:`刷新人物摘要`})]}),(0,x.jsx)(`p`,{className:`muted`,children:`从与此人物相关、全部入口共享的有效记忆提炼。查看与轮询不会召回或增加使用次数。`}),(i.error||ie)&&(0,x.jsx)(A,{error:!0,children:ie||i.error}),oe&&(0,x.jsx)(A,{children:oe}),B?(0,x.jsxs)(x.Fragment,{children:[!B.supported&&(0,x.jsx)(A,{children:`此主体类型或已合并人物不支持摘要写操作,可查阅已有历史。`}),!B.global_enabled&&(0,x.jsx)(A,{children:`全局人物摘要开关已关闭,不会生成或加入回复准备。`}),ce&&(0,x.jsx)(`button`,{disabled:R||i.loading||!!i.error||!ge,onClick:()=>{le(!1),ae(``),k(e=>e?{...e,expected_version:V,original:d.data?.sentences||[]}:null),N(e=>e?{...e,expected_version:V}:null)},children:`已核对最新版本,继续操作`}),(0,x.jsxs)(`p`,{children:[(0,x.jsx)(M,{children:B.settings.enabled?`此人摘要已开启`:`此人摘要已关闭`}),` `,`·`,` `,B.settings.publish_mode===`all_manual`?`全部人工确认`:`检查通过自动发布`,` `,`·`,` `,(0,x.jsxs)(M,{tone:B.pending_count?`amber`:``,children:[`待确认 `,B.pending_count]})]}),B.supported&&(0,x.jsxs)(`div`,{className:`button-row`,children:[(0,x.jsx)(`button`,{disabled:U||!ge,onClick:()=>k({content:B.current?.content||``,expected_version:V,original:d.data?.sentences||[]}),children:`编辑摘要`}),(0,x.jsx)(`button`,{disabled:U,onClick:()=>N({enabled:B.settings.enabled,publish_mode:B.settings.publish_mode,expected_version:V}),children:`人物摘要设置`}),(0,x.jsx)(`button`,{disabled:U||H||!B.global_enabled||!B.settings.enabled,onClick:()=>void be(`/regenerate`,`POST`,{expected_version:V},`已接受重新生成,请查看任务进度。`),children:H?`摘要生成中…`:`重新生成摘要`})]}),(0,x.jsx)(`p`,{className:`fine-print`,children:`重新生成至少需要 8 条相关全局有效记忆,并受开关、版本检查与模型预算限制。`}),j&&(0,x.jsxs)(`form`,{className:`entry-profile-section`,"aria-label":`人物摘要设置`,onSubmit:async e=>{e.preventDefault(),!(U||j.expected_version!==V)&&await be(`/settings`,`PATCH`,j,`人物摘要设置已保存`)&&N(null)},children:[(0,x.jsxs)(`label`,{className:`check-label`,children:[(0,x.jsx)(`input`,{type:`checkbox`,disabled:R,checked:j.enabled,onChange:e=>N({...j,enabled:e.target.checked})}),`启用此人的摘要`]}),(0,x.jsxs)(`label`,{children:[`摘要发布方式`,(0,x.jsxs)(`select`,{disabled:R,value:j.publish_mode,onChange:e=>N({...j,publish_mode:e.target.value}),children:[(0,x.jsx)(`option`,{value:`check_auto`,children:`检查通过自动发布`}),(0,x.jsx)(`option`,{value:`all_manual`,children:`全部人工确认`})]})]}),(0,x.jsx)(`p`,{children:`关闭后保留历史;发布方式只影响之后的候选,不会发布已有待确认版本。`}),j.expected_version!==V&&(0,x.jsx)(A,{children:`设置基于旧版本,请取消后重新打开设置。`}),(0,x.jsxs)(`div`,{className:`button-row`,children:[(0,x.jsx)(`button`,{disabled:U||j.expected_version!==V,children:`保存人物摘要设置`}),(0,x.jsx)(`button`,{type:`button`,disabled:R,onClick:()=>N(null),children:`取消设置`})]})]}),O&&(0,x.jsxs)(`form`,{className:`entry-profile-section`,"aria-label":`编辑人物摘要`,onSubmit:async e=>{e.preventDefault(),!(U||ye||O.expected_version!==V)&&await be(``,`PUT`,{content:O.content,expected_version:O.expected_version},`摘要编辑已发布`)&&k(null)},children:[(0,x.jsx)(`h3`,{children:`编辑并直接发布`}),(0,x.jsx)(`p`,{children:`新增或改动条目标为管理员手写,之后生成会保留;未改条目保留原依据。按句号或换行分条。`}),(0,x.jsxs)(`label`,{children:[`人物摘要正文`,(0,x.jsx)(`textarea`,{rows:6,disabled:R,value:O.content,onChange:e=>k({...O,content:e.target.value})})]}),(0,x.jsxs)(`p`,{children:[_e.items,` / `,ve.max_items,` 条 · `,_e.characters,` /`,` `,ve.max_chars,` 字 · `,_e.tokens,` / `,ve.max_tokens,` `,`估算 token(无最低字数) · 基于 v`,O.expected_version]}),O.expected_version!==V&&(0,x.jsxs)(A,{children:[`当前版本已变化,草稿已保留。请核对最新摘要后更新草稿版本。`,(0,x.jsx)(`button`,{type:`button`,disabled:!ge||U,onClick:()=>k({...O,expected_version:V,original:d.data?.sentences||[]}),children:`更新草稿版本`})]}),(0,x.jsxs)(`div`,{className:`button-row`,children:[(0,x.jsx)(`button`,{disabled:U||ye||O.expected_version!==V,children:`发布摘要编辑`}),(0,x.jsx)(`button`,{type:`button`,disabled:R,onClick:()=>k(null),children:`取消编辑摘要`})]})]}),(0,x.jsxs)(`section`,{className:`entry-profile-section`,"aria-label":`当前人物摘要`,children:[(0,x.jsx)(`h3`,{children:`当前摘要`}),B.current?d.error?(0,x.jsxs)(A,{error:!0,children:[d.error,(0,x.jsx)(`button`,{onClick:d.refresh,children:`重试当前摘要`})]}):!d.data||d.data.version!==V?(0,x.jsx)(`p`,{children:`正在读取当前摘要…`}):(0,x.jsx)(oi,{value:d.data,openMemory:n}):(0,x.jsx)(`p`,{children:`尚无已发布的人物摘要。`}),!d.data&&(0,x.jsx)(ri,{value:B.stale_basis,openMemory:n})]}),B.pending&&(0,x.jsxs)(`section`,{className:`entry-profile-section`,"aria-label":`待确认人物摘要`,children:[(0,x.jsxs)(`h3`,{children:[`待确认候选 · v`,B.pending.version]}),(0,x.jsx)(`p`,{className:`entry-profile-content`,children:B.pending.content}),(0,x.jsxs)(`div`,{className:`button-row`,children:[(0,x.jsxs)(`button`,{onClick:()=>pe(B.pending.version),children:[`审阅候选 v`,B.pending.version]}),B.current&&(0,x.jsx)(`button`,{onClick:()=>xe(V,B.pending.version),children:`对比当前与候选`})]})]}),he&&(0,x.jsx)(li,{base:r,initial:he,onSettled:z,view:pe},he.id),(0,x.jsxs)(`section`,{className:`entry-profile-section`,"aria-label":`人物摘要历史`,children:[(0,x.jsx)(`h3`,{children:`版本历史`}),u.error&&(0,x.jsxs)(A,{error:!0,children:[u.error,(0,x.jsx)(`button`,{onClick:u.refresh,children:`重试摘要历史`})]}),u.data?.items.map(e=>(0,x.jsxs)(`div`,{className:`entry-profile-evidence`,children:[(0,x.jsxs)(`button`,{onClick:()=>pe(e.version),children:[`查看摘要 v`,e.version]}),` `,(0,x.jsx)(M,{children:e.is_current?`当前版本`:Ht[e.status]||e.status}),e.cleared_at&&(0,x.jsx)(M,{tone:`warning`,children:`因彻底清除而清空`}),(0,x.jsx)(`p`,{children:w(e.generated_at)})]},e.version)),u.data&&!u.data.total&&(0,x.jsx)(`p`,{children:`暂无历史版本。`}),u.data&&(0,x.jsx)(T,{total:u.data.total,offset:a,change:o,limit:10})]}),s!=null&&(0,x.jsxs)(`section`,{className:`entry-profile-section`,"aria-label":`人物摘要版本详情`,children:[(0,x.jsxs)(`h3`,{children:[`摘要 v`,s,` 的依据与检查`]}),p.error?(0,x.jsxs)(A,{error:!0,children:[p.error,(0,x.jsx)(`button`,{onClick:p.refresh,children:`重试摘要版本`})]}):p.data?(0,x.jsxs)(x.Fragment,{children:[(0,x.jsx)(oi,{value:p.data,openMemory:n}),B.supported&&p.data.status===`candidate`&&(0,x.jsxs)(`div`,{className:`button-row`,children:[(0,x.jsxs)(`button`,{disabled:U||!!p.data.cleared_at||!p.data.checks.passed,onClick:()=>F({kind:`confirm`,version:s,expected_version:V}),children:[`确认发布摘要 v`,s]}),(0,x.jsxs)(`button`,{disabled:U,onClick:()=>{te(``),F({kind:`reject`,version:s,expected_version:V})},children:[`拒绝摘要 v`,s]})]}),B.supported&&p.data.status===`published`&&!p.data.is_current&&(0,x.jsxs)(`button`,{disabled:U||!!p.data.cleared_at||!!p.data.invalidated_reason||!p.data.content.trim(),onClick:()=>F({kind:`rollback`,version:s,expected_version:V}),children:[`回滚到摘要 v`,s]})]}):(0,x.jsx)(`p`,{children:`正在读取版本…`})]}),(0,x.jsxs)(`section`,{className:`entry-profile-section`,"aria-label":`摘要版本对比`,children:[(0,x.jsx)(`h3`,{children:`任意两版对比`}),(0,x.jsxs)(`form`,{className:`button-row`,onSubmit:e=>{e.preventDefault(),Number.isSafeInteger(+S)&&+S>0&&Number.isSafeInteger(+E)&&+E>0&&xe(+S,+E)},children:[(0,x.jsxs)(`label`,{children:[`较早版本`,(0,x.jsx)(`input`,{type:`number`,min:1,required:!0,value:S,onChange:e=>C(e.target.value)})]}),(0,x.jsxs)(`label`,{children:[`较晚版本`,(0,x.jsx)(`input`,{type:`number`,min:1,required:!0,value:E,onChange:e=>D(e.target.value)})]}),(0,x.jsx)(`button`,{children:`对比摘要版本`})]}),b.error&&(0,x.jsxs)(A,{error:!0,children:[b.error,(0,x.jsx)(`button`,{onClick:b.refresh,children:`重试摘要对比`})]}),b.data&&(0,x.jsxs)(x.Fragment,{children:[(0,x.jsxs)(`p`,{children:[`v`,b.data.before,` → v`,b.data.after,!b.data.changed&&` · 条目没有变化`]}),b.data.sentences.map((e,t)=>(0,x.jsxs)(`div`,{className:`persona-diff-row`,children:[(0,x.jsx)(`strong`,{children:{equal:`保留`,replace:`修改`,insert:`新增`,delete:`删除`}[e.op]||e.op}),(0,x.jsxs)(`div`,{className:`persona-diff-columns`,children:[(0,x.jsxs)(`div`,{children:[(0,x.jsx)(`small`,{children:`之前`}),e.before.map((e,t)=>(0,x.jsx)(`p`,{className:`entry-profile-content`,children:e.text},t))]}),(0,x.jsxs)(`div`,{children:[(0,x.jsx)(`small`,{children:`之后`}),e.after.map((e,t)=>(0,x.jsx)(`p`,{className:`entry-profile-content`,children:e.text},t))]})]})]},t))]})]}),(0,x.jsx)(`a`,{href:`#/operations?object_type=person_summary&object_id=${encodeURIComponent(e)}`,children:`查看人物摘要操作记录`}),P&&(0,x.jsxs)(ee,{title:P.kind===`confirm`?`确认发布人物摘要`:P.kind===`reject`?`拒绝人物摘要`:`回滚人物摘要`,onClose:()=>F(null),closeDisabled:R,children:[(0,x.jsx)(`p`,{children:P.kind===`rollback`?`将复制 v${P.version},新建并发布一个版本;历史版本保持原记录。`:P.kind===`confirm`?`将发布候选 v${P.version},替换当前摘要。`:`将拒绝候选 v${P.version},保留当前发布版。`}),P.kind===`reject`&&(0,x.jsxs)(`label`,{children:[`拒绝摘要原因`,(0,x.jsx)(`input`,{maxLength:1e3,value:I,onChange:e=>te(e.target.value),disabled:R})]}),(0,x.jsx)(`p`,{children:`请核对条目、依据和检查记录后再次确认。`}),ie&&(0,x.jsx)(A,{error:!0,children:ie}),(0,x.jsxs)(`div`,{className:`button-row`,children:[(0,x.jsx)(`button`,{disabled:R,onClick:()=>F(null),children:`取消操作`}),(0,x.jsxs)(`button`,{className:`primary`,disabled:U||P.expected_version!==V,onClick:()=>void be(`/versions/${P.version}/${P.kind}`,`POST`,{expected_version:P.expected_version,...P.kind===`reject`?{reason:I.trim()||`管理员拒绝`}:{}},P.kind===`rollback`?`已新建并发布回滚版本`:P.kind===`confirm`?`候选已确认发布`:`候选已拒绝`),children:[`确认`,P.kind===`rollback`?`回滚`:P.kind===`confirm`?`发布`:`拒绝`]})]})]})]}):(0,x.jsx)(`p`,{role:`status`,children:`正在读取人物摘要…`})]})}function di({initialQuery:e=``,onChange:t,openMemory:n=()=>{}}){let r=new URLSearchParams(e),[i,a]=(0,l.useState)(r.get(`id`)),[o,s]=(0,l.useState)(``),[c,u]=(0,l.useState)({text:``,pending_only:r.get(`pending_only`)===`true`,include_merged:!1}),[d,f]=(0,l.useState)(c),[p,m]=(0,l.useState)(0),h=new URLSearchParams({text:d.text,pending_only:String(d.pending_only),include_merged:String(d.include_merged),limit:`30`,offset:String(p)}),g=y(i?null:`/people?${h}`,5e3);return(0,x.jsxs)(x.Fragment,{children:[(0,x.jsxs)(`div`,{className:`page-heading`,children:[(0,x.jsxs)(`div`,{children:[(0,x.jsx)(`p`,{className:`eyebrow`,children:`名字背后的人`}),(0,x.jsx)(`h1`,{children:`人物`}),(0,x.jsx)(`p`,{children:`核对身份与证据,再决定是否为同一个人。`})]}),(0,x.jsx)(`a`,{className:`button`,href:`#/people-graph`,children:`人物关系图 →`})]}),o&&(0,x.jsx)(A,{children:o}),i?(0,x.jsx)(pi,{id:i,summaryVersion:i===r.get(`id`)&&Number(r.get(`summary_version`))||void 0,openMemory:n,openPerson:a,onBack:()=>a(null),onChange:()=>{g.refresh(),t()},onMerged:e=>{s(`已确认联系并完成人物合并,以下为保留人物的资料。`),a(e)}},i):(0,x.jsxs)(x.Fragment,{children:[(0,x.jsxs)(`form`,{className:`panel people-filters`,onSubmit:e=>{e.preventDefault(),f({...c}),m(0),g.refresh()},children:[(0,x.jsxs)(`label`,{children:[`搜索名字或别名`,(0,x.jsx)(`input`,{maxLength:100,value:c.text,onChange:e=>u({...c,text:e.target.value})})]}),(0,x.jsxs)(`label`,{className:`check-label`,children:[(0,x.jsx)(`input`,{type:`checkbox`,checked:c.pending_only,onChange:e=>u({...c,pending_only:e.target.checked})}),`只看待确认`]}),(0,x.jsxs)(`label`,{className:`check-label`,children:[(0,x.jsx)(`input`,{type:`checkbox`,checked:c.include_merged,onChange:e=>u({...c,include_merged:e.target.checked})}),`包含已合并人物`]}),(0,x.jsx)(`button`,{className:`primary`,children:`搜索人物`})]}),g.error&&(0,x.jsxs)(A,{error:!0,children:[g.error,(0,x.jsx)(`button`,{onClick:g.refresh,children:`重新加载`})]}),g.data?(0,x.jsxs)(x.Fragment,{children:[(0,x.jsxs)(`p`,{className:`muted`,children:[`共 `,g.data.total,` 位人物`]}),!g.data.items.length&&(0,x.jsx)(j,{title:`当前范围没有人物`}),(0,x.jsx)(`div`,{className:`entry-grid`,children:g.data.items.map(e=>(0,x.jsxs)(`article`,{className:`panel person-card`,children:[(0,x.jsx)(`h2`,{children:e.name}),(0,x.jsxs)(`small`,{className:`muted`,children:[`人物标识:`,e.id]}),!!e.pending_links&&(0,x.jsx)(`p`,{children:(0,x.jsxs)(M,{tone:`purple`,children:[`可能是同一人 · `,e.pending_links,` 条待确认`]})}),e.merged_into&&(0,x.jsx)(`p`,{children:(0,x.jsx)(M,{children:`已合并`})}),(0,x.jsxs)(`p`,{children:[`别名:`,e.aliases.map(e=>e.alias).join(`、`)||`暂无`]}),(0,x.jsxs)(`p`,{children:[`相关记忆 `,e.memory_count,` 条`]}),(0,x.jsx)(`button`,{className:`text-button`,onClick:()=>a(e.id),"aria-label":`查看人物 ${e.name}`,children:`查看人物 →`})]},e.id))}),(0,x.jsx)(T,{total:g.data.total,offset:p,change:m})]}):(0,x.jsx)(`p`,{role:`status`,children:`正在读取人物…`})]})]})}function fi({messages:e}){return(0,x.jsxs)(`details`,{className:`person-evidence`,open:!0,children:[(0,x.jsxs)(`summary`,{children:[`证据消息 · `,e.length,` 条`]}),!e.length&&(0,x.jsx)(`p`,{className:`muted`,children:`没有可展示的证据消息。`}),e.map(e=>(0,x.jsxs)(`blockquote`,{children:[(0,x.jsxs)(`small`,{children:[e.sender_name||e.sender_subject_id,` ·`,` `,e.entry_name||e.entry_id,` · #`,e.id,` ·`,` `,w(e.occurred_at||e.received_at)]}),(0,x.jsx)(`p`,{children:e.content}),e.quote_content&&(0,x.jsxs)(`div`,{className:`evidence-quote`,children:[(0,x.jsxs)(`small`,{children:[`引用`,e.quote_author_subject_id?` · 作者标识 ${e.quote_author_subject_id}`:``]}),(0,x.jsx)(`p`,{children:e.quote_content})]})]},e.id))]})}function pi({id:e,summaryVersion:t,openMemory:n,openPerson:r,onBack:i,onChange:a,onMerged:o}){let s=y(`/people/${encodeURIComponent(e)}`),[c,u]=(0,l.useState)(!1),[d,p]=(0,l.useState)(``),[m,h]=(0,l.useState)(null),[b,S]=(0,l.useState)(``),[C,w]=(0,l.useState)(``),[T,D]=(0,l.useState)(!1),[O,k]=(0,l.useState)(!1),j=s.data,N=T||O||s.loading||!!s.error,P=()=>{h(null),S(``),k(!1),s.refresh()},F=e=>{let t=e instanceof f&&e.status===409;k(t),S(t?`资料或联系已变化,请刷新人物资料后重新核对。`:v(e))},I=e=>{h(null),w(e),S(``),s.refresh(),a()};async function te(t){if(t.preventDefault(),!j||N)return;let n=d.trim();if(!n||Array.from(n).length>100){S(`别名需为 1 至 100 个字符`);return}if(n.toLocaleLowerCase()===j.name.trim().toLocaleLowerCase()){S(`别名不能与人物名字相同`);return}D(!0),S(``),w(``);try{await g(`/people/${encodeURIComponent(e)}/aliases`,_(`POST`,{alias:n,expected_revision:j.revision})),p(``),I(`已添加别名`)}catch(e){F(e)}finally{D(!1)}}async function L(){if(j&&m&&m.kind!==`merge`&&!N){D(!0),S(``),w(``);try{m.kind===`deny`?await g(`/people/links/${m.link.id}/deny`,_(`POST`,{expected_revision:m.link.revision})):await g(`/people/${encodeURIComponent(e)}/aliases/${m.alias.id}`,_(`DELETE`,{expected_revision:j.revision})),I(m.kind===`deny`?`联系已否认,后续学习不会重新提出这对人物的同一人联系。`:`已删除别名,后续学习不会自动加回。必要时可手动添加。`)}catch(e){F(e)}finally{D(!1)}}}return(0,x.jsxs)(`section`,{className:`person-detail`,children:[(0,x.jsxs)(`div`,{className:`button-row`,children:[(0,x.jsx)(`button`,{className:`text-button`,onClick:i,children:`← 返回人物列表`}),(0,x.jsx)(`button`,{disabled:T||s.loading,onClick:P,children:`刷新人物资料`})]}),(s.error||b)&&!m&&(0,x.jsx)(A,{error:!0,children:b||s.error}),C&&(0,x.jsx)(A,{children:C}),j?(0,x.jsxs)(x.Fragment,{children:[c&&(0,x.jsx)(Gr,{scope:{subject_id:j.id},name:j.name,onClose:()=>u(!1),onChanged:()=>{s.refresh(),a()}}),(0,x.jsxs)(`section`,{className:`panel`,children:[(0,x.jsx)(`h2`,{children:j.name}),!j.merged_into&&(0,x.jsx)(`button`,{className:`secondary`,disabled:N,onClick:()=>u(!0),children:`批量遗忘/删除`}),(0,x.jsxs)(`p`,{className:`muted`,children:[`人物标识:`,j.id,` · 资料修订 `,j.revision]}),j.merged_into&&(0,x.jsxs)(A,{children:[`此人物已合并,原标识保留用于追溯。`,(0,x.jsx)(`button`,{onClick:()=>r(j.canonical_id),children:`查看合并后的人物`})]}),(0,x.jsx)(ui,{subjectId:e,initialVersion:t,openMemory:n}),(0,x.jsx)(`h3`,{children:`平台身份`}),!j.platform_identities.length&&(0,x.jsx)(`p`,{className:`muted`,children:`暂无平台身份。`}),j.platform_identities.map(e=>(0,x.jsxs)(`dl`,{className:`identity-facts`,children:[(0,x.jsxs)(`div`,{children:[(0,x.jsx)(`dt`,{children:`平台`}),(0,x.jsx)(`dd`,{children:e.platform===`iris-trial`?`试用`:e.platform})]}),(0,x.jsxs)(`div`,{children:[(0,x.jsx)(`dt`,{children:`账号`}),(0,x.jsx)(`dd`,{children:e.account_id})]}),(0,x.jsxs)(`div`,{children:[(0,x.jsx)(`dt`,{children:`显示名`}),(0,x.jsx)(`dd`,{children:e.display_name})]})]},`${e.platform}:${e.account_id}`)),(0,x.jsxs)(`h3`,{children:[`相关记忆 · `,j.memory_count,` 条`]}),(0,x.jsx)(`p`,{children:Object.entries(j.memory_counts).map(([e,t])=>`${E(e)} ${t} 条`).join(` · `)||`暂无相关记忆`}),(0,x.jsx)(`a`,{href:`#/memories?${new URLSearchParams({person_id:j.canonical_id,lifecycle:`all`})}`,children:`查看关于此人的记忆`})]}),(0,x.jsxs)(`section`,{className:`panel`,children:[(0,x.jsx)(`h3`,{children:`别名`}),!j.aliases.length&&(0,x.jsx)(`p`,{className:`muted`,children:`暂无别名。`}),(0,x.jsx)(`ul`,{className:`alias-list`,children:j.aliases.map(e=>(0,x.jsxs)(`li`,{children:[(0,x.jsx)(`span`,{children:e.alias}),!!e.evidence_message_ids.length&&(0,x.jsxs)(`small`,{className:`muted`,children:[`证据消息`,` `,e.evidence_message_ids.map(e=>`#${e}`).join(`、`)]}),!j.merged_into&&(0,x.jsx)(`button`,{disabled:N,className:`text-button danger-text`,"aria-label":`删除别名 ${e.alias}`,onClick:()=>{S(``),h({kind:`alias`,alias:e})},children:`删除`})]},e.id))}),!j.merged_into&&(0,x.jsxs)(`form`,{className:`inline-form`,onSubmit:te,noValidate:!0,children:[(0,x.jsxs)(`label`,{children:[`新别名`,(0,x.jsx)(`input`,{value:d,maxLength:100,disabled:N,onChange:e=>p(e.target.value)})]}),(0,x.jsx)(`button`,{disabled:N,className:`secondary`,children:`添加别名`})]})]}),(0,x.jsxs)(`section`,{className:`panel`,children:[(0,x.jsx)(`h3`,{children:`可能是同一人`}),!j.same_as.length&&(0,x.jsx)(`p`,{className:`muted`,children:`暂无身份联系。`}),j.same_as.map(t=>(0,x.jsxs)(`article`,{className:`person-link`,children:[(0,x.jsx)(`h4`,{children:t.subjects.map(e=>e.name).join(` ↔ `)}),(0,x.jsxs)(`p`,{children:[(0,x.jsx)(M,{tone:t.status===`possible`?`purple`:``,children:t.status===`possible`?`待确认`:t.status===`denied`?`已否认`:`已确认`}),` `,`相信程度 `,t.belief,` / 100`]}),(0,x.jsx)(`div`,{className:`button-row`,children:t.subjects.filter(t=>t.id!==e).map(e=>(0,x.jsxs)(`button`,{className:`text-button`,onClick:()=>r(e.id),children:[`查看 `,e.name,` 的资料`]},e.id))}),(0,x.jsx)(fi,{messages:t.evidence_messages}),t.status===`possible`&&!j.merged_into&&(0,x.jsxs)(`div`,{className:`button-row`,children:[(0,x.jsx)(`button`,{disabled:N,className:`primary`,onClick:()=>{S(``),h({kind:`merge`,link:t})},children:`确认并合并`}),(0,x.jsx)(`button`,{disabled:N,onClick:()=>{S(``),h({kind:`deny`,link:t})},children:`否认联系`})]})]},t.id))]}),(0,x.jsxs)(`section`,{className:`panel`,children:[(0,x.jsx)(`h3`,{children:`扮演关系`}),!j.roleplay.length&&(0,x.jsx)(`p`,{className:`muted`,children:`暂无扮演关系。`}),j.roleplay.map(e=>(0,x.jsxs)(`article`,{className:`person-link`,children:[(0,x.jsxs)(`h4`,{children:[e.actor.name,` 扮演 `,e.character.name]}),(0,x.jsxs)(`p`,{children:[(0,x.jsx)(M,{children:`虚构角色`}),` · 相信程度 `,e.belief,` / 100`]}),(0,x.jsxs)(`p`,{children:[`所属场景:`,e.worlds.map(e=>e??`场景未知`).join(`、`)]}),(0,x.jsx)(`p`,{className:`muted`,children:`扮演关系不代表同一人,角色经历不归入现实人物。`}),(0,x.jsx)(fi,{messages:e.evidence_messages})]},e.id))]}),m?.kind===`merge`?(0,x.jsx)(mi,{link:m.link,initialTarget:e,onClose:()=>h(null),onRefresh:P,onSuccess:e=>{I(``),o(e)}}):m&&(0,x.jsxs)(ee,{title:m.kind===`deny`?`否认人物联系`:`删除别名`,onClose:()=>h(null),closeDisabled:T,children:[m.kind===`deny`?(0,x.jsxs)(`p`,{children:[`否认 `,m.link.subjects.map(e=>e.name).join(`与`),` `,`是同一人。否认后,后续学习不会改回待确认,也不会再次提出这对人物的同一人联系。`]}):(0,x.jsxs)(`p`,{children:[`删除别名“`,m.alias.alias,`”及其别名证据记录,后续学习不会自动加回。管理员仍可手动添加。`]}),b&&(0,x.jsxs)(A,{error:!0,children:[b,O&&(0,x.jsx)(`button`,{onClick:P,children:`刷新人物资料`})]}),(0,x.jsxs)(`div`,{className:`button-row`,children:[(0,x.jsx)(`button`,{onClick:()=>h(null),disabled:T,children:`取消`}),(0,x.jsx)(`button`,{className:`danger-button`,disabled:N,onClick:L,children:m.kind===`deny`?`确认否认`:`确认删除别名`})]})]})]}):(0,x.jsx)(`p`,{role:`status`,children:`正在读取人物资料…`})]})}function mi({link:e,initialTarget:t,onClose:n,onRefresh:r,onSuccess:i}){let a=y(`/people/${encodeURIComponent(e.subjects[0].id)}`),o=y(`/people/${encodeURIComponent(e.subjects[1].id)}`),[s,c]=(0,l.useState)(t),[u,d]=(0,l.useState)(!1),[p,m]=(0,l.useState)(!1),[h,b]=(0,l.useState)(!1),[S,C]=(0,l.useState)(``),w=a.data&&o.data?[a.data,o.data]:null,T=w?.find(e=>e.id===s),E=w?.find(e=>e.id!==s),D=!!a.error||!!o.error||a.loading||o.loading||w?.some(e=>!!e.merged_into);async function O(){if(T&&E&&!p&&!h&&!D&&u){m(!0),C(``);try{i((await g(`/people/links/${e.id}/confirm`,_(`POST`,{target_id:T.id,expected_revision:e.revision,expected_source_revision:E.revision,expected_target_revision:T.revision}))).target_id)}catch(e){let t=e instanceof f&&e.status===409;b(t),C(t?`资料或联系已变化,请刷新人物资料后重新核对。`:v(e))}finally{m(!1)}}}return(0,x.jsxs)(ee,{title:`确认并合并人物`,onClose:n,closeDisabled:p,children:[(S||a.error||o.error)&&(0,x.jsxs)(A,{error:!0,children:[S||a.error||o.error,(0,x.jsx)(`button`,{disabled:p,onClick:r,children:`刷新人物资料`})]}),w?(0,x.jsxs)(x.Fragment,{children:[w.some(e=>e.merged_into)&&(0,x.jsxs)(A,{error:!0,children:[`人物已合并,请刷新后核对。`,(0,x.jsx)(`button`,{onClick:r,children:`刷新人物资料`})]}),(0,x.jsxs)(`fieldset`,{className:`lifecycle-fieldset`,disabled:p||h||!!D,children:[(0,x.jsxs)(`label`,{children:[`保留的人物`,(0,x.jsx)(`select`,{value:s,onChange:e=>{c(e.target.value),d(!1)},children:w.map(e=>(0,x.jsxs)(`option`,{value:e.id,children:[e.name,` · `,e.id]},e.id))})]}),(0,x.jsxs)(`div`,{className:`purge-confirm`,children:[(0,x.jsxs)(`strong`,{children:[`将“`,E?.name,`”合并到“`,T?.name,`”`]}),(0,x.jsx)(`p`,{children:`平台身份、有效别名、记忆的涉及人和说话人、人物联系及从属人物会转到保留方。被合并方的名字成为别名,原人物保留为合并占位。`}),(0,x.jsx)(`p`,{children:`记忆正文、修订号与分数保持原值。联系冲突时否认优先。M2 不提供撤销合并,请先核对双方账号与证据。`}),(0,x.jsxs)(`p`,{children:[E?.name,`:`,E?.platform_identities.length,` `,`个平台身份,`,E?.memory_count,` 条相关记忆。`]}),(0,x.jsxs)(`p`,{children:[T?.name,`:`,T?.platform_identities.length,` 个平台身份,`,T?.memory_count,` 条相关记忆。`]}),w.map(e=>(0,x.jsxs)(`div`,{className:`merge-identity`,children:[(0,x.jsxs)(`strong`,{children:[e.name,` · `,e.id]}),(0,x.jsxs)(`p`,{children:[`账号:`,e.platform_identities.map(e=>`${e.platform} / ${e.account_id}`).join(`;`)||`未绑定平台账号`]}),(0,x.jsxs)(`p`,{children:[`别名:`,e.aliases.map(e=>e.alias).join(`、`)||`暂无`]})]},e.id)),(0,x.jsxs)(`label`,{className:`check-label`,children:[(0,x.jsx)(`input`,{type:`checkbox`,checked:u,onChange:e=>d(e.target.checked)}),`我已核对身份,理解合并后无法在界面撤销`]})]})]}),(0,x.jsxs)(`div`,{className:`button-row`,children:[(0,x.jsx)(`button`,{disabled:p,onClick:n,children:`取消`}),(0,x.jsx)(`button`,{className:`danger-button`,disabled:p||h||!!D||!u,onClick:O,children:p?`正在合并…`:`确认合并`})]})]}):(0,x.jsx)(`p`,{role:`status`,children:`正在核对双方资料…`})]})}var hi=e=>JSON.stringify([e.source,e.target]),gi=e=>`${e.links.some(e=>e.kind===`same_as`)?`可能同一人`:e.links.some(e=>e.kind===`roleplay`)?`扮演`:e.relationship_count?`关系`:`共现`} · ${e.memory_count}`;function _i(e,t=900){let n=t<560,r=n?48:88,i=n?104:180,a=n?110:135,o=n?24:36,s=n?64:80,c=Math.max(1,Math.ceil(Math.sqrt(e*(n?1:1.7)))),l=Math.max(260,t,e>12?r*2+(c-1)*i:0),u=Math.min(c,Math.max(1,Math.floor((l-r*2)/i)+1)),d=Math.max(1,Math.ceil(e/u));return{width:l,height:Math.max(e<=3?220:n?330:400,o+s+(d-1)*a),compact:n,marginX:r,top:o,bottom:s,rows:d}}function vi(e,t,n=900){if(!e.length)return{};let r=_i(e.length,n),i=e.map((t,n)=>({x:200*Math.cos(n*2*Math.PI/e.length),y:200*Math.sin(n*2*Math.PI/e.length)})),a=new Map(e.map((e,t)=>[e.id,t]));for(let e=0;e<90;e++){let n=i.map(e=>({x:-e.x*.009,y:-e.y*.009}));for(let e=0;e{e.x+=Math.max(-r,Math.min(r,n[t].x)),e.y+=Math.max(-r,Math.min(r,n[t].y))})}let o=[];for(let t=0;te.x),c=i.map(e=>e.y),l=Math.min(...s),u=Math.min(...c),d=Math.max(...s)-l,f=Math.max(...c)-u;return Object.fromEntries(e.map((e,t)=>{let n=d>.001?r.marginX+(i[t].x-l)/d*(r.width-r.marginX*2):r.width/2,a=f>.001?r.top+(i[t].y-u)/f*(r.height-r.top-r.bottom):(r.top+r.height-r.bottom)/2,s=0;for(let e=1;ee.leftt.left&&e.topt.top;function bi(e,t,n,r){let i=e.map(e=>{let t=n[e.id];return{left:t.x-(r.compact?48:84),right:t.x+(r.compact?48:84),top:t.y-(r.compact?22:33),bottom:t.y+(r.compact?57:70)}}),a={};for(let e of t){let t=n[e.source],o=n[e.target],s=o.x-t.x,c=o.y-t.y,l=Math.max(1,Math.hypot(s,c)),u=r.compact?11:12,d=Array.from(gi(e)).reduce((e,t)=>e+u*(t.charCodeAt(0)>255?1:.65),12),f=u+10,p=!1;for(let n of[.5,.35,.65,.2,.8]){for(let o of[-16,16,-32,32,-48,48]){let u={x:t.x+s*n-c/l*o,y:t.y+c*n+s/l*o},m={left:u.x-d/2,right:u.x+d/2,top:u.y-f/2,bottom:u.y+f/2};if(!(m.left<4||m.right>r.width-4||m.top<4||m.bottom>r.height-4||i.some(e=>yi(m,e)))){a[hi(e)]=u,i.push(m),p=!0;break}}if(p)break}}return a}var xi=(e,t)=>Array.from(e).length>t?Array.from(e).slice(0,t).join(``)+`…`:e,Si=e=>`#/memories?person_id=${encodeURIComponent(e)}&lifecycle=active`,Ci=e=>new URLSearchParams(Object.entries(e).filter(([,e])=>e)).toString();function wi({edge:e}){return(0,x.jsxs)(`span`,{className:`graph-kinds`,children:[e.memory_count>0&&(0,x.jsxs)(M,{children:[`共同涉及 `,e.memory_count]}),e.relationship_count>0&&(0,x.jsxs)(M,{tone:`purple`,children:[`关系记忆 `,e.relationship_count]}),e.links.some(e=>e.kind===`same_as`)&&(0,x.jsx)(M,{tone:`purple`,children:`可能同一人 · 未确认`}),e.links.some(e=>e.kind===`roleplay`)&&(0,x.jsx)(M,{children:`扮演 · 虚构角色`})]})}function Ti({data:e,onEdge:t}){let n=(0,l.useRef)(null),[r,i]=(0,l.useState)(900),[a,o]=(0,l.useState)(null),[s,c]=(0,l.useState)(null),u=(0,l.useId)();(0,l.useEffect)(()=>{let e=n.current;if(!e)return;let t=()=>{let t=Math.floor(e.getBoundingClientRect().width);t>0&&i(t)};if(t(),typeof ResizeObserver>`u`)return;let r=new ResizeObserver(t);return r.observe(e),()=>r.disconnect()},[]);let d=(0,l.useMemo)(()=>_i(e.nodes.length,r),[e.nodes.length,r]),f=(0,l.useMemo)(()=>vi(e.nodes,e.edges,r),[e,r]),p=(0,l.useMemo)(()=>bi(e.nodes,e.edges,f,d),[e,f,d]),m=new Map(e.nodes.map(e=>[e.id,e.name])),h=e.edges.find(e=>hi(e)===(s||a));return(0,x.jsxs)(x.Fragment,{children:[(0,x.jsx)(`div`,{ref:n,className:`graph-scroll`,tabIndex:0,"aria-label":`关系图画布,可横向滚动`,children:(0,x.jsxs)(`svg`,{viewBox:`0 0 ${d.width} ${d.height}`,style:{minWidth:d.width},role:`group`,"aria-label":`人物关系图`,className:`people-graph-svg${d.compact?` compact`:``}`,children:[(0,x.jsx)(`title`,{children:`人物关系图`}),(0,x.jsx)(`desc`,{children:`点击节点打开人物的全部有效记忆,点击连线查看依据。悬停或聚焦连线查看摘要,也可使用画布下方的人物和连线列表。`}),e.edges.map(e=>{let n=f[e.source],r=f[e.target],i=`查看${m.get(e.source)}与${m.get(e.target)}的连线依据`,a=hi(e),s=p[a],l=e.links.some(e=>e.kind===`same_as`)?`possible`:e.links.length?`roleplay`:e.relationship_count?`relationship`:`cooccurrence`;return(0,x.jsxs)(`g`,{className:`graph-edge ${l}`,role:`button`,tabIndex:0,"aria-label":i,"aria-describedby":h===e?u:void 0,onMouseEnter:()=>o(a),onMouseLeave:()=>o(null),onFocus:()=>c(a),onBlur:()=>c(null),onClick:()=>t(e),onKeyDown:n=>{(n.key===`Enter`||n.key===` `)&&(n.preventDefault(),t(e))},children:[(0,x.jsxs)(`title`,{children:[i,`;`,e.memory_count,` 条共同涉及记忆`]}),(0,x.jsx)(`line`,{x1:n.x,y1:n.y,x2:r.x,y2:r.y,className:`graph-edge-hit`}),(0,x.jsx)(`line`,{x1:n.x,y1:n.y,x2:r.x,y2:r.y,strokeWidth:Math.min(7,1.5+Math.log2(1+e.memory_count))}),s&&(0,x.jsx)(`text`,{x:s.x,y:s.y,textAnchor:`middle`,dominantBaseline:`middle`,children:gi(e)})]},a)}),e.nodes.map(e=>(0,x.jsxs)(`a`,{href:Si(e.id),"aria-label":`查看${e.name}的有效记忆`,className:`graph-node ${e.kind===`self`?`self`:``}`,children:[(0,x.jsxs)(`title`,{children:[e.name,`;别名:`,e.aliases.join(`、`)||`暂无`,`;`,e.account_count,` 个账号;`,e.memory_count,` 条有效记忆`]}),(0,x.jsx)(`circle`,{cx:f[e.id].x,cy:f[e.id].y,r:d.compact?14:Math.min(27,18+Math.log2(1+e.memory_count))}),(0,x.jsx)(`text`,{x:f[e.id].x,y:f[e.id].y+5,textAnchor:`middle`,className:`graph-node-mark`,children:e.kind===`self`?`我`:Array.from(e.name)[0]}),(0,x.jsx)(`rect`,{className:`graph-node-label-background`,x:f[e.id].x-(d.compact?44:78),y:f[e.id].y+(d.compact?20:30),width:d.compact?88:156,height:d.compact?33:38,rx:`4`}),(0,x.jsx)(`text`,{x:f[e.id].x,y:f[e.id].y+(d.compact?32:44),textAnchor:`middle`,children:xi(e.name,d.compact?6:10)}),(0,x.jsxs)(`text`,{x:f[e.id].x,y:f[e.id].y+(d.compact?48:63),textAnchor:`middle`,className:`graph-node-count`,children:[e.memory_count,` 条记忆`]})]},e.id))]})}),(0,x.jsx)(`div`,{className:`graph-preview`,children:h?(0,x.jsxs)(`div`,{id:u,role:`tooltip`,children:[(0,x.jsxs)(`strong`,{children:[m.get(h.source),` · `,m.get(h.target)]}),(0,x.jsxs)(`p`,{children:[gi(h),`(数字为共同涉及的有效记忆数)`]}),h.relationship_summaries.map(e=>(0,x.jsx)(`p`,{children:e.summary},e.id)),!h.relationship_summaries.length&&(0,x.jsx)(`p`,{children:`点击连线查看联系记录与支撑记忆。`})]}):(0,x.jsx)(`p`,{children:`悬停或用键盘聚焦连线可查看摘要;数字为共同涉及的有效记忆数。拥挤处省略标签,完整内容见下方列表。`})})]})}function Ei({edge:e,names:t,entryId:n,onClose:r,openMemory:i}){let[a,o]=(0,l.useState)(0),s=y(`/people-graph/edge?${Ci({source_id:e.source,target_id:e.target,entry_id:n,limit:`30`,offset:String(a)})}`);return(0,x.jsxs)(ee,{title:`连线依据:${t.get(e.source)} · ${t.get(e.target)}`,onClose:r,children:[(0,x.jsx)(`p`,{children:`这里只显示当前仍有效的记忆。身份/扮演联系来自独立联系记录;同来源记忆不等于确认身份,也不把角色经历归给扮演者。`}),s.error&&(0,x.jsxs)(A,{error:!0,children:[s.error,(0,x.jsx)(`button`,{onClick:s.refresh,children:`重新读取依据`})]}),s.loading&&(0,x.jsx)(`p`,{role:`status`,children:`正在读取连线依据…`}),s.data&&(0,x.jsxs)(x.Fragment,{children:[s.data.links.map(e=>(0,x.jsxs)(`article`,{className:`graph-link-record`,children:[(0,x.jsx)(`h3`,{children:e.kind===`same_as`?`可能同一人 · 未确认`:`扮演 · 虚构角色`}),(0,x.jsxs)(`p`,{children:[t.get(e.source),` `,e.kind===`roleplay`?`→`:`↔`,` `,t.get(e.target),` · 相信程度 `,e.belief]}),e.kind===`roleplay`&&(0,x.jsxs)(`p`,{children:[`场景:`,e.worlds.map(e=>e||`场景未记录`).join(`、`)]}),(0,x.jsxs)(`p`,{children:[`联系 #`,e.id,` · `,e.evidence_message_count,` 条来源消息`]}),(0,x.jsx)(`a`,{href:`#/people?id=${encodeURIComponent(e.source)}`,onClick:r,children:`到人物详情核对联系与消息`})]},e.id)),(0,x.jsxs)(`p`,{children:[`共 `,s.data.total,` 条支撑/同来源记忆`]}),!s.data.items.length&&(0,x.jsx)(j,{title:`没有可展示的有效支撑记忆`,children:`联系记录可能只有来源消息;遗忘、删除或清除的记忆不会在这里显示。`}),(0,x.jsx)(`div`,{className:`graph-evidence-list`,children:s.data.items.map(e=>(0,x.jsxs)(`article`,{children:[(0,x.jsxs)(`p`,{children:[(0,x.jsx)(M,{children:e.kind}),` #`,e.id,` · 修订`,` `,e.revision]}),(0,x.jsx)(`p`,{className:`memory-content`,children:e.content}),(0,x.jsxs)(`p`,{children:[e.cooccurrence&&(0,x.jsx)(M,{children:`共同涉及两人`}),` `,e.link_evidence&&(0,x.jsx)(M,{children:`同来源联系证据`})]}),(0,x.jsxs)(`button`,{onClick:()=>{r(),i(e.id)},children:[`查看记忆 #`,e.id]})]},e.id))}),(0,x.jsx)(T,{total:s.data.total,offset:a,change:o})]})]})}function Di({initialQuery:e=``,openMemory:t}){let n=new URLSearchParams(e),[r,i]=(0,l.useState)({entry_id:n.get(`entry_id`)||``,person_id:n.get(`person_id`)||``,hops:`1`,min_memories:`1`,node_limit:`60`,edge_limit:`100`}),[a,o]=(0,l.useState)(r),[s,c]=(0,l.useState)(null),u=y(`/catalog`),d=y(`/people-graph?${Ci(a)}`),f=new Map(d.data?.nodes.map(e=>[e.id,e.name])||[]),p=e=>{c(null),o({...e}),Ci(e)===Ci(a)&&d.refresh()};return(0,x.jsxs)(x.Fragment,{children:[(0,x.jsxs)(`div`,{className:`page-heading`,children:[(0,x.jsxs)(`div`,{children:[(0,x.jsx)(`p`,{className:`eyebrow`,children:`从已有经历看人物联系`}),(0,x.jsx)(`h1`,{children:`人物关系图`}),(0,x.jsx)(`p`,{children:`点击人物查看记忆,点击连线追溯依据。`})]}),(0,x.jsx)(`a`,{href:`#/people`,className:`button`,children:`返回人物列表`})]}),(0,x.jsxs)(`form`,{className:`panel graph-filters`,onSubmit:e=>{e.preventDefault(),p(r)},children:[(0,x.jsxs)(`label`,{children:[`来源入口`,(0,x.jsxs)(`select`,{value:r.entry_id,onChange:e=>i({...r,entry_id:e.target.value}),children:[(0,x.jsx)(`option`,{value:``,children:`全部入口`}),u.data?.entries.map(e=>(0,x.jsx)(`option`,{value:e.id,children:e.name},e.id))]})]}),(0,x.jsxs)(`label`,{children:[`聚焦人物`,(0,x.jsxs)(`select`,{value:r.person_id,onChange:e=>i({...r,person_id:e.target.value}),children:[(0,x.jsx)(`option`,{value:``,children:`所有人物`}),u.data?.people.filter(e=>e.id!==`scene`).map(e=>(0,x.jsxs)(`option`,{value:e.id,children:[e.name,e.id===`self`?`(角色)`:``,` · `,e.id]},e.id))]})]}),(0,x.jsxs)(`label`,{children:[`范围`,(0,x.jsxs)(`select`,{value:r.hops,disabled:!r.person_id,onChange:e=>i({...r,hops:e.target.value}),children:[(0,x.jsx)(`option`,{value:`1`,children:`一跳联系`}),(0,x.jsx)(`option`,{value:`2`,children:`两跳联系`})]})]}),(0,x.jsxs)(`label`,{children:[`共现最少记忆数`,(0,x.jsx)(`input`,{type:`number`,min:`1`,max:`1000000`,required:!0,value:r.min_memories,onChange:e=>i({...r,min_memories:e.target.value})})]}),(0,x.jsxs)(`label`,{children:[`节点上限`,(0,x.jsx)(`input`,{type:`number`,min:`2`,max:`100`,required:!0,value:r.node_limit,onChange:e=>i({...r,node_limit:e.target.value})})]}),(0,x.jsxs)(`label`,{children:[`连线上限`,(0,x.jsx)(`input`,{type:`number`,min:`1`,max:`200`,required:!0,value:r.edge_limit,onChange:e=>i({...r,edge_limit:e.target.value})})]}),(0,x.jsx)(`button`,{className:`primary`,disabled:d.loading,children:`生成关系图`})]}),u.error&&(0,x.jsxs)(A,{error:!0,children:[`筛选目录:`,u.error,(0,x.jsx)(`button`,{onClick:u.refresh,children:`重新读取目录`})]}),(0,x.jsxs)(`details`,{className:`panel graph-explanation`,children:[(0,x.jsx)(`summary`,{children:`数据口径与图例`}),(0,x.jsx)(`p`,{children:`只统计 active 且未彻底清除的记忆。共现按同一记忆的 about 涉及人两两计数,每条只算一次;共现不等于关系事实。“关系”类记忆另外显示正文摘要。`}),(0,x.jsx)(`p`,{children:`入口筛选只认至少一条直接来源消息在该入口的记忆,不沿派生依据扩大范围。身份/扮演联系按其来源消息筛选,可没有支撑记忆;共现阈值不隐藏这些独立联系。`}),(0,x.jsx)(`p`,{children:`节点记忆数包括作为涉及人或说话人的有效记忆,别名与账号数为当前主体资料。self 显示为角色节点,scene 不作为人物;已合并主体只显示保留人物。点击人物打开其全部入口的有效记忆列表。`}),(0,x.jsx)(`p`,{children:`线条越粗,共同涉及记忆越多;紫色为关系记忆,虚线为可能同一人,绿色为扮演。位置和距离只用于排布,没有关系强弱含义。图中读数不调用模型,不产生召回或使用记录。`})]}),d.error&&(0,x.jsxs)(A,{error:!0,children:[d.error,(0,x.jsx)(`button`,{onClick:d.refresh,children:`重新读取关系图`})]}),d.loading&&(0,x.jsx)(`p`,{role:`status`,children:`正在生成只读关系图…`}),d.data&&(0,x.jsxs)(x.Fragment,{children:[(0,x.jsxs)(`p`,{"aria-live":`polite`,children:[`当前显示 `,d.data.nodes.length,` 个节点、`,d.data.edges.length,` `,`条连线。`]}),d.data.truncated&&(0,x.jsxs)(A,{children:[`筛选后共有 `,d.data.total_nodes,` 个节点、`,d.data.total_edges,` `,`条连线,超过显示上限。按共同涉及记忆数从高到低保留,关系记忆数和主体标识用于同权重排序;聚焦人物优先保留。可缩小入口或聚焦范围后重新查看。`]}),d.data.nodes.length?(0,x.jsxs)(x.Fragment,{children:[(0,x.jsxs)(`section`,{className:`panel graph-panel`,"aria-label":`关系图与图例`,children:[(0,x.jsxs)(`div`,{className:`graph-legend`,children:[(0,x.jsx)(`span`,{children:`实线:共现`}),(0,x.jsx)(`span`,{children:`紫色:关系记忆`}),(0,x.jsx)(`span`,{children:`虚线:可能同一人`}),(0,x.jsx)(`span`,{children:`绿色:扮演`})]}),(0,x.jsx)(Ti,{data:d.data,onEdge:c}),(0,x.jsx)(`p`,{className:`muted`,children:`窄屏可横向滚动画布。下方列表提供相同的跳转和证据操作。`})]}),(0,x.jsxs)(`section`,{"aria-label":`图中人物`,children:[(0,x.jsx)(`h2`,{children:`图中人物`}),(0,x.jsx)(`div`,{className:`graph-node-list`,children:d.data.nodes.map(e=>(0,x.jsxs)(`article`,{className:`panel`,children:[(0,x.jsxs)(`h3`,{children:[(0,x.jsx)(`a`,{href:Si(e.id),"aria-label":`查看${e.name}的有效记忆`,children:e.name}),` `,e.kind===`self`&&(0,x.jsx)(M,{children:`角色`})]}),(0,x.jsx)(`small`,{className:`muted`,children:e.id}),(0,x.jsxs)(`p`,{children:[`别名:`,e.aliases.join(`、`)||`暂无`]}),(0,x.jsxs)(`p`,{children:[e.account_count,` 个账号 · `,e.memory_count,` `,`条有效记忆`]}),(0,x.jsx)(`button`,{className:`text-button`,"aria-label":`聚焦${e.name}`,onClick:()=>{let t={...r,person_id:e.id};i(t),p(t)},children:`以此人为中心`})]},e.id))})]}),(0,x.jsxs)(`section`,{"aria-label":`图中连线`,children:[(0,x.jsx)(`h2`,{children:`图中连线`}),!d.data.edges.length&&(0,x.jsx)(`p`,{className:`muted`,children:`当前阈值和范围没有连线。`}),(0,x.jsx)(`div`,{className:`graph-edge-list`,children:d.data.edges.map(e=>(0,x.jsxs)(`article`,{className:`panel`,children:[(0,x.jsxs)(`h3`,{children:[f.get(e.source),` · `,f.get(e.target)]}),(0,x.jsx)(wi,{edge:e}),(0,x.jsxs)(`p`,{children:[e.memory_count,` 条共同涉及记忆`]}),e.relationship_summaries.map(e=>(0,x.jsxs)(`p`,{children:[`关系记忆 #`,e.id,`:`,e.summary]},e.id)),e.relationship_count>e.relationship_summaries.length&&(0,x.jsx)(`small`,{children:`更多关系正文见连线依据。`}),(0,x.jsx)(`button`,{onClick:()=>c(e),"aria-label":`查看${f.get(e.source)}与${f.get(e.target)}的连线依据`,children:`查看连线依据`})]},JSON.stringify([e.source,e.target])))})]})]}):(0,x.jsx)(j,{title:`当前范围没有人物关系数据`,children:`可以改选入口或人物;遗忘、删除和清除的记忆不参与关系图。`})]}),s&&(0,x.jsx)(Ei,{edge:s,names:f,entryId:a.entry_id,onClose:()=>c(null),openMemory:t},JSON.stringify([s.source,s.target,a.entry_id]))]})}function Oi({initialVersion:e,initialComparison:t,currentID:n,catalog:r,openMemory:i,onAction:a,locked:o}){let[s,c]=(0,l.useState)(e),[u,d]=(0,l.useState)({status:``,source:``}),[f,p]=(0,l.useState)(``),[m,h]=(0,l.useState)(0),g=y(`/persona/versions?${f}limit=30&offset=${m}`,5e3),[_,v]=(0,l.useState)(t?.before.toString()||``),[b,S]=(0,l.useState)(t?.after.toString()||n?.toString()||``),[C,w]=(0,l.useState)(``),[E,D]=(0,l.useState)(t||null),[O,k]=(0,l.useState)(0);return(0,x.jsxs)(x.Fragment,{children:[(0,x.jsxs)(`section`,{className:`panel persona-history`,children:[(0,x.jsx)(`h2`,{children:`版本历史`}),(0,x.jsx)(`p`,{children:`可选择任意两个版本逐句对比。只有已经发布过的版本可以回滚;回滚会新建版本。`}),(0,x.jsxs)(`form`,{className:`persona-filters`,onSubmit:e=>{e.preventDefault();let t=new URLSearchParams;u.status&&t.set(`status`,u.status),u.source&&t.set(`source`,u.source),p(t.size?`${t}&`:``),h(0),g.refresh()},children:[(0,x.jsxs)(`label`,{children:[`版本状态`,(0,x.jsxs)(`select`,{value:u.status,onChange:e=>d({...u,status:e.target.value}),children:[(0,x.jsx)(`option`,{value:``,children:`全部状态`}),Object.entries(ne).map(([e,t])=>(0,x.jsx)(`option`,{value:e,children:t},e))]})]}),(0,x.jsxs)(`label`,{children:[`版本来源`,(0,x.jsxs)(`select`,{value:u.source,onChange:e=>d({...u,source:e.target.value}),children:[(0,x.jsx)(`option`,{value:``,children:`全部来源`}),Object.entries(L).map(([e,t])=>(0,x.jsx)(`option`,{value:e,children:t},e))]})]}),(0,x.jsx)(`button`,{children:`筛选版本`})]}),g.error&&(0,x.jsxs)(A,{error:!0,children:[g.error,(0,x.jsx)(`button`,{onClick:g.refresh,children:`重试版本列表`})]}),g.loading&&(0,x.jsx)(`p`,{children:`正在读取版本…`}),g.data?.items.map(e=>(0,x.jsxs)(`article`,{className:`persona-history-row`,children:[(0,x.jsxs)(`div`,{children:[(0,x.jsxs)(`strong`,{children:[`v`,e.id]}),` `,(0,x.jsx)(M,{children:ne[e.status]}),` ·`,` `,L[e.source],` · 变化`,` `,e.change_degree?R[e.change_degree]:`未记录`,(0,x.jsx)(`p`,{className:`muted`,children:(0,x.jsx)(N,{value:e.generated_at})}),(0,x.jsx)(`p`,{className:`persona-content`,children:e.content})]}),(0,x.jsxs)(`div`,{className:`persona-actions`,children:[(0,x.jsxs)(`button`,{onClick:()=>c(e.id),children:[`查看版本 v`,e.id]}),(0,x.jsxs)(`button`,{onClick:()=>v(String(e.id)),children:[`v`,e.id,` 作为对比前版`]}),(0,x.jsxs)(`button`,{onClick:()=>S(String(e.id)),children:[`v`,e.id,` 作为对比后版`]})]})]},e.id)),g.data?.total===0&&(0,x.jsx)(j,{title:`没有符合筛选的版本`}),g.data&&(0,x.jsx)(T,{total:g.data.total,offset:m,change:h})]}),(0,x.jsxs)(`section`,{className:`panel persona-comparison`,children:[(0,x.jsx)(`h2`,{children:`对比任意版本`}),(0,x.jsx)(`p`,{children:`从列表选择,或输入版本 ID;可以跨页选择,也可以查看被拒绝的候选。`}),(0,x.jsxs)(`form`,{noValidate:!0,className:`persona-filters`,onSubmit:e=>{if(e.preventDefault(),!/^\d+$/.test(_)||!/^\d+$/.test(b)||!Number.isSafeInteger(Number(_))||!Number.isSafeInteger(Number(b))||Number(_)<1||Number(b)<1){w(`请输入有效的正整数版本 ID`);return}w(``),D({before:Number(_),after:Number(b)}),k(e=>e+1)},children:[(0,x.jsxs)(`label`,{children:[`对比前版本 ID`,(0,x.jsx)(`input`,{type:`number`,min:1,step:1,value:_,onChange:e=>v(e.target.value)})]}),(0,x.jsxs)(`label`,{children:[`对比后版本 ID`,(0,x.jsx)(`input`,{type:`number`,min:1,step:1,value:b,onChange:e=>S(e.target.value)})]}),(0,x.jsx)(`button`,{children:`对比版本`})]}),C&&(0,x.jsx)(A,{error:!0,children:C}),E&&(0,x.jsx)(Qt,{...E},O)]}),s&&(0,x.jsx)(Xt,{id:s,title:`版本详情`,catalog:r,openMemory:i,onAction:a,locked:o,currentID:n,history:!0},s)]})}function ki({openMemory:e}){let[t,n]=(0,l.useState)(0),r=y(`/persona/self-memories?limit=30&offset=${t}`);return(0,x.jsxs)(`section`,{className:`panel persona-memories`,children:[(0,x.jsx)(`h2`,{children:`自我记忆`}),(0,x.jsx)(`p`,{children:`有效的自我设定与经历是 persona 的材料;打开或翻页不会触发召回。`}),r.error&&(0,x.jsxs)(A,{error:!0,children:[r.error,(0,x.jsx)(`button`,{onClick:r.refresh,children:`重试自我记忆`})]}),r.loading&&(0,x.jsx)(`p`,{children:`正在读取自我记忆…`}),r.data?.items.map(t=>(0,x.jsxs)(`article`,{className:`persona-history-row`,children:[(0,x.jsx)(`p`,{className:`persona-content`,children:t.content}),(0,x.jsxs)(`p`,{className:`muted`,children:[`#`,t.id,` · 修订 `,t.revision,` · `,t.stance,` ·`,` `,E(t.lifecycle),` · `,(0,x.jsx)(N,{value:t.created_at})]}),(0,x.jsxs)(`button`,{className:`text-button`,onClick:()=>e(t.id),children:[`查看自我记忆 #`,t.id]})]},t.id)),r.data?.total===0&&(0,x.jsx)(j,{title:`暂无有效自我记忆`,children:`在持续对话中积累经历,或在设置中完善初始背景。`}),r.data&&(0,x.jsx)(T,{total:r.data.total,offset:t,change:n})]})}function Ai(){let[e,t]=(0,l.useState)(0),n=y(`/persona/attempts?limit=30&offset=${e}`,2e3);return(0,x.jsxs)(`section`,{className:`panel persona-attempts`,children:[(0,x.jsx)(`h2`,{children:`生成记录`}),(0,x.jsx)(`p`,{children:`包括未生成版本的跳过、冲突与失败;生成被拒绝不改变当前 persona。`}),n.error&&(0,x.jsxs)(A,{error:!0,children:[n.error,(0,x.jsx)(`button`,{onClick:n.refresh,children:`重试生成记录`})]}),n.loading&&(0,x.jsx)(`p`,{children:`正在读取生成记录…`}),n.data?.items.map(e=>(0,x.jsxs)(`article`,{className:`persona-history-row`,children:[(0,x.jsxs)(`h3`,{children:[`任务 #`,e.id,` · `,se(e.state,e.stage)]}),(0,x.jsxs)(`p`,{children:[L[e.source]||e.source,` · 基于 v`,e.base_version_id]}),(0,x.jsxs)(`p`,{className:`muted`,children:[`接受:`,(0,x.jsx)(N,{value:e.created_at}),` · 结束:`,(0,x.jsx)(N,{value:e.finished_at})]}),e.reason&&(0,x.jsx)(`p`,{children:ae(e.reason)}),e.version_id&&(0,x.jsxs)(`a`,{href:`#/persona?tab=history&version=${e.version_id}`,children:[`查看版本 v`,e.version_id]}),(0,x.jsx)(`p`,{children:(0,x.jsx)(`a`,{href:`#/operations?object_type=persona_attempt&object_id=${e.id}`,children:`查看任务操作记录`})})]},e.id)),n.data?.total===0&&(0,x.jsx)(j,{title:`暂无生成任务`}),n.data&&(0,x.jsx)(T,{total:n.data.total,offset:e,change:t})]})}function ji({initial:e,path:t,onFinish:n}){let[r,i]=(0,l.useState)(e),[a,o]=(0,l.useState)(``),[s,c]=(0,l.useState)(0),u=(0,l.useRef)(n);u.current=n;let d=oe(e.state);(0,l.useEffect)(()=>{if(!d){i(e),o(``);return}let n=new AbortController,r=!0,a,s=async()=>{let c=!0;try{let a=await g(t||`/persona/attempts/${e.id}`,{signal:n.signal});if(!r)return;i(a),o(``),oe(a.state)||(c=!1,u.current(a))}catch(e){r&&!n.signal.aborted&&o(v(e))}r&&c&&(a=setTimeout(s,2e3))};return s(),()=>{r=!1,n.abort(),clearTimeout(a)}},[e.id,d,t,s]);let f=y(r.state===`rejected`&&r.version_id?`/persona/versions/${r.version_id}`:null);return(0,x.jsxs)(`section`,{className:`panel persona-task`,"aria-label":`最近生成任务`,children:[(0,x.jsxs)(`h2`,{children:[`生成任务 #`,r.id]}),(0,x.jsxs)(`p`,{role:`status`,children:[se(r.state,r.stage),r.state===`current`&&r.version_id?` · v${r.version_id}`:``]}),oe(r.state)&&(0,x.jsx)(`p`,{children:`请求已接受,尚未发布。页面会持续查询生成与检查结果;离开后任务仍在服务端运行。`}),r.reason&&(0,x.jsx)(A,{error:r.state===`failed`||r.state===`conflict`,children:ae(r.reason)}),(0,x.jsxs)(`p`,{className:`muted`,children:[`基于 v`,r.base_version_id,` · 接受时间:`,(0,x.jsx)(N,{value:r.created_at}),r.finished_at&&(0,x.jsxs)(x.Fragment,{children:[` `,`· 结束时间:`,(0,x.jsx)(N,{value:r.finished_at})]})]}),a&&(0,x.jsxs)(A,{error:!0,children:[a,`。任务结果尚未获知,将继续查询。`,(0,x.jsx)(`button`,{onClick:()=>c(e=>e+1),children:`立即查询任务`})]}),f.error&&(0,x.jsxs)(A,{error:!0,children:[f.error,(0,x.jsx)(`button`,{onClick:f.refresh,children:`重试拒绝原因`})]}),f.data&&(0,x.jsxs)(`div`,{children:[(0,x.jsx)(`h3`,{children:`拒绝原因`}),f.data.rejection_reasons.length?(0,x.jsx)(`ul`,{children:f.data.rejection_reasons.map((e,t)=>(0,x.jsx)(`li`,{children:ae(e)},t))}):(0,x.jsx)(`p`,{children:`未记录拒绝原因,请查看版本检查结果。`})]}),r.version_id&&(0,x.jsxs)(`a`,{href:`#/persona?tab=history&version=${r.version_id}`,children:[`查看结果版本 v`,r.version_id]}),(0,x.jsx)(`p`,{children:(0,x.jsx)(`a`,{href:`#/operations?object_type=persona_attempt&object_id=${r.id}`,children:`查看生成操作记录`})})]})}var Mi={edit:`编辑并发布 persona`,confirm:`确认发布候选`,reject:`拒绝候选`,rollback:`回滚 persona`},Ni={edit:`发布编辑`,confirm:`确认发布`,reject:`确认拒绝`,rollback:`确认回滚`},Pi={current:`当前与候选`,history:`版本历史`,memories:`自我记忆`,attempts:`生成记录`};function Fi({openMemory:e,initialQuery:t=``}){let n=new URLSearchParams(t),r=Number(n.get(`version`)),[i,a]=(0,l.useState)(n.get(`focus`)===`pending`),o=(0,l.useRef)(null),[s,c]=(0,l.useState)(Object.hasOwn(Pi,n.get(`tab`)||``)?n.get(`tab`):`current`),u=y(`/persona`,2e3),d=y(`/catalog`),[p,h]=(0,l.useState)(null),[b,S]=(0,l.useState)(``),[C,w]=(0,l.useState)(``),[T,E]=(0,l.useState)(``),[D,O]=(0,l.useState)(``),[k,N]=(0,l.useState)(!1),[P,F]=(0,l.useState)(!1),[I,te]=(0,l.useState)(0),[L,ne]=(0,l.useState)(null),R=u.data,re=R?.current,ie=R?.pending;(0,l.useEffect)(()=>{i&&s===`current`&&ie&&o.current&&(o.current.focus(),o.current.scrollIntoView?.({block:`start`}),a(!1))},[i,s,ie]);let se=R?.latest_attempt,ce=se&&(!L||se.id>L.attempt.id||se.id===L.attempt.id&&oe(L.attempt.state)&&!oe(se.state))?{attempt:se,path:L?.attempt.id===se.id?L.path:void 0}:L,le=k||P||!!u.error||u.loading;function ue(e,t){re&&!le&&(h({kind:e,version:t,expectedVersion:re.id}),S(t.content),w(``),E(``),O(``))}function de(){h(null),F(!1),E(``),O(``),u.refresh(),te(e=>e+1)}function z(e){E(v(e)),e instanceof f&&e.code===`persona_conflict`&&F(!0)}async function B(){if(p&&!le){if(p.kind===`edit`&&(!b.trim()||[...b].length>800)){E(`persona 正文须为 1—800 字,不能只含空白`);return}if(p.kind===`reject`&&[...C.trim()].length>1e3){E(`拒绝原因不能超过 1000 字`);return}N(!0),E(``);try{let e={expected_version:p.expectedVersion,...p.kind===`edit`?{content:b}:p.kind===`reject`&&C.trim()?{reason:C.trim()}:{}},t=await g(p.kind===`edit`?`/persona`:`/persona/versions/${p.version.id}/${p.kind}`,_(p.kind===`edit`?`PUT`:`POST`,e));O(p.kind===`reject`?`已拒绝候选 v${t.id},当前版本仍保留。`:`已发布 persona v${t.id}${p.kind===`rollback`?`,回滚自 v${p.version.id}`:``}。`),h(null),c(`current`),u.refresh(),te(e=>e+1)}catch(e){z(e)}finally{N(!1)}}}async function V(){if(!(!re||le||oe(ce?.attempt.state))){N(!0),E(``),O(``);try{let e=await m(`/persona/regenerate`,_(`POST`,{expected_version:re.id}));if(e.status!==202||!e.data.accepted)throw new f(`invalid_response`,`生成请求返回了意外结果,请刷新查看任务记录;不要重复提交。`,e.status);let t=e.data.attempt,n=e.headers.get(`Location`),r=n===`/admin/api/persona/attempts/${t.id}`?n.slice(10):`/persona/attempts/${t.id}`;ne({attempt:t,path:r}),u.refresh()}catch(e){z(e),u.refresh()}finally{N(!1)}}}let fe=(0,x.jsx)(`button`,{disabled:k,onClick:de,children:`重新加载当前版本`});return(0,x.jsxs)(`div`,{className:`persona-page`,children:[(0,x.jsxs)(`div`,{className:`page-heading`,children:[(0,x.jsxs)(`div`,{children:[(0,x.jsx)(`span`,{className:`eyebrow`,children:`自我与未来`}),(0,x.jsx)(`h1`,{children:`persona 与自我`}),(0,x.jsx)(`p`,{children:`从持续经历中认识自己,逐句查看变化的依据。`})]}),(0,x.jsxs)(`div`,{className:`persona-actions`,children:[(0,x.jsx)(`button`,{disabled:k,onClick:u.refresh,children:`刷新 persona`}),re&&(0,x.jsxs)(x.Fragment,{children:[(0,x.jsx)(`button`,{disabled:le,onClick:()=>ue(`edit`,re),children:`编辑并发布`}),(0,x.jsx)(`button`,{className:`primary`,disabled:le||oe(ce?.attempt.state),onClick:()=>void V(),children:`重新生成`})]})]})]}),u.error&&(0,x.jsxs)(A,{error:!0,children:[u.error,`。数据可能尚未更新,写操作暂不可用。`]}),d.error&&(0,x.jsxs)(A,{error:!0,children:[`入口类型暂不可用。`,d.error,(0,x.jsx)(`button`,{onClick:d.refresh,children:`重试入口信息`})]}),!p&&T&&(0,x.jsxs)(A,{error:!0,children:[T,P&&(0,x.jsxs)(x.Fragment,{children:[(0,x.jsx)(`p`,{children:`请重新加载后核对当前版本,再决定是否操作。`}),fe]})]}),D&&(0,x.jsx)(A,{children:D}),R?.needs_update&&(0,x.jsxs)(`section`,{className:`notice persona-stale`,"aria-label":`失效依据`,children:[(0,x.jsxs)(`h2`,{children:[(0,x.jsx)(M,{tone:`warning`,children:`待更新`}),` `,R.stale_basis_count,` `,`条依据失效`]}),(0,x.jsx)(`p`,{children:`当前 persona 仍在使用。重新生成会重新提炼与检查;失效依据不等于已有待确认候选。`}),(0,x.jsx)(`ul`,{children:R.stale_basis.map((t,n)=>(0,x.jsxs)(`li`,{children:[`记忆 #`,t.memory_id,` · 记录修订 `,t.expected_revision,` ·`,` `,t.current_revision==null?`当前不可用`:`当前修订 ${t.current_revision}`,`:`,ae(t.reason),t.memory&&(0,x.jsxs)(`button`,{className:`text-button`,onClick:()=>e(t.memory_id),children:[`查看失效依据 #`,t.memory_id]})]},`${t.memory_id}-${n}`))})]}),ie&&(0,x.jsxs)(A,{children:[(0,x.jsx)(M,{tone:`warning`,children:`待确认`}),` 候选 v`,ie.id,` `,`等待决定,当前版本仍在使用。`,(0,x.jsx)(`button`,{className:`text-button`,onClick:()=>{c(`current`),a(!0)},children:`查看候选与差异`})]}),R&&!ie&&n.get(`focus`)===`pending`&&(0,x.jsx)(A,{children:`当前没有待确认的 persona 候选。`}),ce&&(0,x.jsx)(ji,{initial:ce.attempt,path:ce.path,onFinish:e=>{ne({attempt:e,path:ce.path}),u.refresh(),te(e=>e+1)}},ce.attempt.id),(0,x.jsx)(U,{}),(0,x.jsx)(`div`,{className:`tabs persona-tabs`,role:`tablist`,"aria-label":`persona 内容`,children:Object.entries(Pi).map(([e,t])=>(0,x.jsx)(`button`,{id:`persona-tab-${e}`,role:`tab`,"aria-selected":s===e,tabIndex:s===e?0:-1,onKeyDown:t=>{let n=Object.keys(Pi),r=n.indexOf(e),i=t.key===`ArrowRight`?(r+1)%n.length:t.key===`ArrowLeft`?(r+n.length-1)%n.length:t.key===`Home`?0:t.key===`End`?n.length-1:null;i!==null&&(t.preventDefault(),c(n[i]),document.getElementById(`persona-tab-${n[i]}`)?.focus())},"aria-controls":`persona-tab-panel`,className:s===e?`active`:``,onClick:()=>c(e),children:t},e))}),(0,x.jsxs)(`div`,{role:`tabpanel`,id:`persona-tab-panel`,"aria-labelledby":`persona-tab-${s}`,children:[s===`current`&&(0,x.jsxs)(x.Fragment,{children:[!R&&!u.error&&(0,x.jsx)(`p`,{children:`正在读取 persona…`}),R&&!re&&(0,x.jsx)(j,{title:`暂无 persona 版本`,children:`请先在设置中完成角色初始设定。`}),re&&(0,x.jsx)(Xt,{id:re.id,title:`当前 persona`,catalog:d.data,openMemory:e},`current-${re.id}-${I}`),ie&&(0,x.jsxs)(`div`,{ref:o,role:`region`,"aria-label":`待确认候选与差异`,tabIndex:-1,children:[(0,x.jsx)(Xt,{id:ie.id,title:`待确认候选`,catalog:d.data,openMemory:e,onAction:ue,currentID:re?.id,locked:le},`pending-${ie.id}-${I}`),re&&(0,x.jsx)(`div`,{className:`panel`,children:(0,x.jsx)(Qt,{before:re.id,after:ie.id},`${re.id}-${ie.id}-${I}`)})]})]}),s===`history`&&(0,x.jsx)(Oi,{initialComparison:(()=>{let e=Number(n.get(`before`)),t=Number(n.get(`after`));return Number.isSafeInteger(e)&&e>0&&Number.isSafeInteger(t)&&t>0?{before:e,after:t}:void 0})(),initialVersion:Number.isSafeInteger(r)&&r>0?r:void 0,currentID:re?.id,catalog:d.data,openMemory:e,onAction:ue,locked:le}),s===`memories`&&(0,x.jsx)(ki,{openMemory:e}),s===`attempts`&&(0,x.jsx)(Ai,{})]}),p&&(0,x.jsx)(ee,{title:Mi[p.kind],closeDisabled:k,onClose:()=>h(null),children:(0,x.jsxs)(`form`,{noValidate:!0,className:`persona-action-form`,onSubmit:e=>{e.preventDefault(),B()},children:[p.kind===`edit`?(0,x.jsxs)(x.Fragment,{children:[(0,x.jsxs)(`p`,{children:[`发布会替换当前 v`,p.expectedVersion,` `,`并新建版本;新增或改动的句子会标为手写。待确认候选会被替代。`]}),(0,x.jsx)(`p`,{children:`之后的自动候选若删去手写内容,按“大变化”处理:默认方式需人工确认;“全部自动”模式下,检查通过仍会直接发布。`}),(0,x.jsxs)(`label`,{children:[`persona 正文`,(0,x.jsx)(`textarea`,{rows:9,disabled:k,value:b,onChange:e=>S(e.target.value)})]}),(0,x.jsxs)(`small`,{children:[[...b].length,` / 800 字`]})]}):p.kind===`confirm`?(0,x.jsxs)(`p`,{children:[`发布候选 v`,p.version.id,` 会替换当前 v`,p.expectedVersion,`。旧版本保留在历史中,可在之后回滚;服务会再次核对版本和依据。`]}):p.kind===`reject`?(0,x.jsxs)(x.Fragment,{children:[(0,x.jsxs)(`p`,{children:[`拒绝候选 v`,p.version.id,`,保留当前 v`,p.expectedVersion,`。拒绝记录与原因会保存;采用新候选需要再次生成。`]}),(0,x.jsxs)(`label`,{children:[`拒绝原因(可选)`,(0,x.jsx)(`textarea`,{rows:3,maxLength:1e3,disabled:k,value:C,onChange:e=>w(e.target.value)})]})]}):(0,x.jsxs)(`p`,{children:[`使用 v`,p.version.id,` 的正文与依据新建一个版本,替换当前 v`,p.expectedVersion,`;历史版本保持不变。待确认候选会被替代,旧依据失效时新版本仍会标为待更新。`]}),T&&(0,x.jsxs)(A,{error:!0,children:[T,P&&(0,x.jsxs)(x.Fragment,{children:[(0,x.jsx)(`p`,{children:`重新加载会放弃本次未保存草稿。可先复制草稿,再核对最新版本。`}),(0,x.jsx)(`button`,{type:`button`,onClick:de,children:`重新加载当前版本`})]})]}),(0,x.jsxs)(`div`,{className:`persona-actions`,children:[(0,x.jsx)(`button`,{type:`button`,disabled:k,onClick:()=>h(null),children:`取消`}),(0,x.jsx)(`button`,{className:`primary`,disabled:le,children:k?`正在提交…`:Ni[p.kind]})]})]})})]})}var Ii=[{id:`trial`,name:`试用对话`,icon:`chat`},{id:`memories`,name:`记忆`,icon:`memory`},{id:`people`,name:`人物`,icon:`people`},{id:`learning`,name:`入口与学习`,icon:`learning`},{id:`persona`,name:`persona 与自我`,icon:`people`},{id:`state`,name:`状态与目标`,icon:`state`},{id:`status`,name:`运行状态`,icon:`status`},{id:`operations`,name:`操作记录`,icon:`memory`},{id:`connections`,name:`接入与数据`,icon:`learning`},{id:`settings`,name:`设置`,icon:`settings`}];function Li({name:e}){return(0,x.jsx)(`svg`,{viewBox:`0 0 24 24`,fill:`none`,stroke:`currentColor`,strokeWidth:`1.7`,strokeLinecap:`round`,strokeLinejoin:`round`,"aria-hidden":!0,children:e===`chat`?(0,x.jsx)(`path`,{d:`M20 11.5a8 8 0 0 1-8 8H5l-3 2 1.5-5A8 8 0 1 1 20 11.5ZM7 10h8M7 14h5`}):e===`people`?(0,x.jsxs)(x.Fragment,{children:[(0,x.jsx)(`circle`,{cx:`9`,cy:`7`,r:`3`}),(0,x.jsx)(`path`,{d:`M3 21v-3a6 6 0 0 1 12 0v3M16 4a3 3 0 0 1 0 6M18 14a5 5 0 0 1 3 4v3`})]}):e===`learning`?(0,x.jsx)(x.Fragment,{children:(0,x.jsx)(`path`,{d:`M4 4h16v5H4zM4 14h7v6H4zM16 14h4v6h-4M8 9v5M18 9v5`})}):e===`state`?(0,x.jsxs)(x.Fragment,{children:[(0,x.jsx)(`circle`,{cx:`12`,cy:`12`,r:`9`}),(0,x.jsx)(`path`,{d:`M12 7v5l3 2`})]}):e===`memory`?(0,x.jsxs)(x.Fragment,{children:[(0,x.jsx)(`rect`,{x:`5`,y:`3`,width:`14`,height:`18`,rx:`3`}),(0,x.jsx)(`path`,{d:`M9 8h6M9 12h6M9 16h3`})]}):(0,x.jsx)(x.Fragment,{children:(0,x.jsx)(`path`,{d:`M3 13h4l3-7 4 13 3-7h4`})})})}function Ri(){return(0,x.jsx)(Ie,{children:e=>(0,x.jsx)(zi,{refreshSession:e})})}function zi({refreshSession:e}){let[t,n]=(0,l.useState)(location.hash.slice(2)||`trial`),[r,i]=(0,l.useState)(null),[a,o]=(0,l.useState)(0),s=y(`/status`,2e3),c=y(`/people?pending_only=true&limit=1&offset=0`,5e3),u=t.split(`?`)[0],d=()=>{o(e=>e+1),s.refresh()};(0,l.useEffect)(()=>{let e=()=>{n(location.hash.slice(2)||`trial`),i(null)};return addEventListener(`hashchange`,e),()=>removeEventListener(`hashchange`,e)},[]);let f=(0,l.useCallback)(e=>i(e),[]),p=(0,l.useCallback)(()=>i(null),[]),m=s.data?.model_health.chat;return(0,x.jsxs)(`div`,{className:`app`,children:[(0,x.jsx)(`a`,{className:`skip-link`,href:`#main`,onClick:e=>{e.preventDefault(),document.getElementById(`main`)?.focus()},children:`跳到主内容`}),(0,x.jsxs)(`header`,{className:`app-header`,children:[(0,x.jsxs)(`a`,{href:`#/trial`,className:`brand`,"aria-label":`Iris 首页`,children:[(0,x.jsx)(`span`,{className:`brand-symbol`,"aria-hidden":!0,children:`✳`}),(0,x.jsxs)(`span`,{children:[`Iris`,(0,x.jsx)(`small`,{children:`相处与记忆`})]})]}),(0,x.jsx)(`nav`,{"aria-label":`主导航`,children:Ii.map(e=>(0,x.jsxs)(`a`,{href:`#/${e.id}`,"aria-current":u===e.id||u===`people-graph`&&e.id===`people`?`page`:void 0,children:[(0,x.jsx)(Li,{name:e.icon}),e.name,e.id===`people`&&!!c.data?.total&&(0,x.jsx)(`span`,{className:`nav-dot`,"aria-label":`有待确认的人物联系`})]},e.id))}),(0,x.jsx)(`button`,{className:`text-button logout-button`,onClick:async()=>{await g(`/logout`,_(`POST`,{})),e()},children:`退出登录`}),(0,x.jsxs)(`span`,{className:`local-badge`,children:[(0,x.jsx)(`span`,{"aria-hidden":!0,children:`◉`}),` 仅本机`]})]}),(0,x.jsx)(`main`,{id:`main`,tabIndex:-1,children:(0,x.jsxs)(`div`,{className:`content-wrap`,children:[!!c.data?.total&&u===`trial`&&(0,x.jsxs)(A,{children:[c.data.total,` 位人物有待确认的“可能是同一人”联系。`,(0,x.jsx)(`a`,{href:`#/people?pending_only=true`,children:`查看待确认联系`})]}),c.error&&u===`trial`&&(0,x.jsxs)(A,{error:!0,children:[`人物联系提示暂时无法更新。`,(0,x.jsx)(`button`,{onClick:c.refresh,children:`重试人物提示`})]}),s.error&&(0,x.jsxs)(A,{error:!0,children:[s.error,`。以下数据可能尚未更新。`,(0,x.jsx)(`button`,{className:`text-button`,onClick:s.refresh,children:`重新连接`})]}),m&&m.state!==`normal`&&(0,x.jsxs)(A,{error:!0,children:[`对话模型:`,D(m.state),`。`,m.last_error?.includes(`未配置`)?`未配置模型,暂不学习。`:`学习已暂停。`,`消息仍可接收。`,(0,x.jsx)(`a`,{href:`#/status`,children:`查看运行状态`})]}),u===`settings`?(0,x.jsx)(Ne,{}):u===`memories`?(0,x.jsx)(hn,{initialQuery:t.split(`?`)[1],version:a,openMemory:f,onChange:d},t):u===`people-graph`?(0,x.jsx)(Di,{initialQuery:t.split(`?`)[1],openMemory:f},t):u===`people`?(0,x.jsx)(di,{openMemory:f,initialQuery:t.split(`?`)[1],onChange:()=>{c.refresh(),d()}},t):u===`operations`?(0,x.jsx)(we,{initialQuery:t.split(`?`)[1]},t):u===`learning`?(0,x.jsx)(Zr,{openMemory:f}):u===`state`?(0,x.jsx)(Un,{initialQuery:t.split(`?`)[1],openMemory:f},t):u===`persona`?(0,x.jsx)(Fi,{initialQuery:t.split(`?`)[1],openMemory:f},t):u===`connections`?(0,x.jsx)(Ue,{}):u===`status`?(0,x.jsx)(or,{initialQuery:t.split(`?`)[1]||``,data:s.data,openMemory:f},t):(0,x.jsx)(Gn,{status:s.data,openMemory:f,refreshStatus:s.refresh})]})}),(0,x.jsx)(`footer`,{className:`app-footer`,children:`Iris · 记忆有来源,相处有连续`}),r!==null&&(0,x.jsx)(_n,{id:r,onClose:p,openMemory:f,onChange:d},r)]})}(0,c.createRoot)(document.getElementById(`root`)).render((0,x.jsx)(l.StrictMode,{children:(0,x.jsx)(Ri,{})})); \ No newline at end of file diff --git a/src/iris/web/index.html b/src/iris/web/index.html index 8f99c7c7..5a6d2628 100644 --- a/src/iris/web/index.html +++ b/src/iris/web/index.html @@ -1,6 +1,6 @@ - Iris · 相处与记忆 + Iris · 相处与记忆
diff --git a/tests/goal_auto_helpers.py b/tests/goal_auto_helpers.py new file mode 100644 index 00000000..daf7b0e9 --- /dev/null +++ b/tests/goal_auto_helpers.py @@ -0,0 +1,118 @@ +"""Deterministic fixtures for the G line (daily goal auto review); no network.""" +import json +from datetime import datetime +from itertools import count + +from iris.goals import AUTO_REVIEW_KEY, DUPLICATE_AUTO_KEY +from iris.maintenance import Maintenance, PHASES + +CREATED = '2026-11-02T09:13:00+00:00' # 17:13 in Asia/Shanghai +NOW = '2026-11-03T01:00:00+00:00' +_keys = count(1) + + +class Clock: + def __init__(self, value=NOW): + self.value = datetime.fromisoformat(value) + + def __call__(self): + return self.value + + +class Reviewer: + """Fake chat gateway for G1. Records every material; asserts no open transaction.""" + + def __init__(self, store, answer=None, hook=None): + self.store, self.answer, self.hook = store, answer, hook + self.calls = [] + + def json_chat(self, messages, purpose, max_tokens=3500, *, batch_id=None): + assert not self.store._writer.in_transaction + payload = json.loads(messages[-1]['content']) + self.calls.append({'purpose': purpose, 'payload': payload, 'system': messages[0]['content'], + 'max_tokens': max_tokens}) + if self.hook: + self.hook(payload) + answer = self.answer(payload) if callable(self.answer) else self.answer + if isinstance(answer, Exception): + raise answer + return answer, json.dumps(answer, ensure_ascii=False), None, 'direct', None + + +def cite(decision, pick=lambda payload: [payload['messages'][-1]['id']]): + return lambda payload: {'decision': decision, 'evidence': pick(payload), 'reason': '测试'} + + +def subject(store, sid, name): + with store.write() as conn: + conn.execute("INSERT OR IGNORE INTO subjects(id,kind,name,created_at) VALUES(?,'person',?,?)", (sid, name, CREATED)) + + +def entry(conn, eid, visibility='shared'): + conn.execute("INSERT OR IGNORE INTO entries(id,name,platform,kind,visibility) VALUES(?,?,'unit','private',?)", + (eid, eid, visibility)) + + +def message(store, content, *, sender='p1', at='2026-11-02T19:08:00+08:00', kind='message', entry_id='e1', + quote_author=None, quote=None): + with store.write() as conn: + entry(conn, entry_id) + return conn.execute('''INSERT INTO messages(entry_id,kind,sender_subject_id,content,quote_author_subject_id, + quote_content,occurred_at,received_at,dedupe_key,learning_state) VALUES(?,?,?,?,?,?,?,?,?,'learned')''', + (entry_id, kind, sender, content, quote_author, quote, at, at, f'k{next(_keys)}')).lastrowid + + +def goal(store, content='帮程栗取南门驿站的电热毯快递', *, people=('p1',), sources=(), origin='internal', + kind='normal', created=CREATED, entry_id='e1', deadline=None): + with store.write() as conn: + entry(conn, entry_id) + gid = conn.execute('''INSERT INTO goals(content,kind,origin,state,deadline,deadline_at,entry_id,created_at, + updated_at,schedule_initialized) VALUES(?,?,?,'open',?,?,?,?,?,1)''', + (content, kind, origin, deadline, deadline, entry_id, created, created)).lastrowid + conn.executemany('INSERT INTO goal_people(goal_id,subject_id) VALUES(?,?)', [(gid, p) for p in people]) + conn.executemany('INSERT INTO goal_sources(goal_id,message_id) VALUES(?,?)', [(gid, m) for m in sources]) + return gid + + +def basic(store): + """One learning goal with a source before creation and one later candidate.""" + subject(store, 'p1', '程栗') + subject(store, 'p2', '路人') + source = message(store, '能帮我取一下南门驿站的电热毯吗', at='2026-11-02T17:10:00+08:00') + gid = goal(store, sources=[source]) + done = message(store, '到家了,餐桌上的电热毯收到了,谢谢!', at='2026-11-02T19:08:00+08:00') + return gid, source, done + + +def enable(store, *, review=True, duplicates=False): + store.set_setting('goals', {AUTO_REVIEW_KEY: review, DUPLICATE_AUTO_KEY: duplicates}) + + +def run_goals(store, gateway, clock=None, *, stop=None): + """One maintenance run whose model phases are disabled; jumps to the goal phase.""" + clock = clock or Clock() + store.set_setting('consolidation', {'enabled': False}) + store.set_setting('scheduled_backup', {'enabled': False}) + engine = Maintenance(store, gateway=gateway, clock=clock) + rid = engine.request() + with store.write() as conn: + conn.execute('UPDATE maintenance_runs SET phase=? WHERE id=?', (PHASES.index('goals'), rid)) + engine.run(rid, stop=stop) + return engine, rid + + +def row(store, gid): + with store.read() as conn: + return dict(conn.execute('SELECT * FROM goals WHERE id=?', (gid,)).fetchone()) + + +def reviews(store, gid=None): + with store.read() as conn: + return [dict(r) for r in conn.execute('SELECT * FROM goal_auto_reviews' + (' WHERE goal_id=?' if gid else '') + + ' ORDER BY id', (gid,) if gid else ())] + + +def items(store, rid): + with store.read() as conn: + return [{**dict(r), 'details': json.loads(r['details_json'])} for r in conn.execute( + "SELECT * FROM maintenance_items WHERE run_id=? AND phase='goals' ORDER BY rowid", (rid,))] diff --git a/tests/test_goal_auto_review.py b/tests/test_goal_auto_review.py new file mode 100644 index 00000000..dbffa0fc --- /dev/null +++ b/tests/test_goal_auto_review.py @@ -0,0 +1,817 @@ +"""G1: learning-created goals closed only on explicit, code-checked evidence.""" +import json + +import pytest + +from iris.goals import (AUTO_REVIEW_KEY, AUTO_REVIEW_PURPOSE, DUPLICATE_AUTO_KEY, GoalConflict, GoalError, Goals, + auto_review_prompt, goal_settings) +from iris.models import ModelError +from goal_auto_helpers import (CREATED, Clock, Reviewer, basic, cite, enable, entry, goal, items, message, reviews, + row, run_goals, subject) + + +def test_switches_default_off_and_no_model_call(store): + with store.read() as conn: + settings = goal_settings(conn) + assert settings[AUTO_REVIEW_KEY] is False and settings[DUPLICATE_AUTO_KEY] is False + gid, _, _ = basic(store) + reviewer = Reviewer(store, cite('completed')) + engine, rid = run_goals(store, reviewer) + assert reviewer.calls == [] and row(store, gid)['state'] == 'open' and reviews(store) == [] + report = engine.report(rid) + assert report['goal_auto_review'] == {'enabled': False, 'skip_reason': 'disabled'} + assert report['goal_duplicate_review']['enabled'] is False + + +def test_explicit_completion_closes_with_evidence_snapshot_and_report(store): + gid, source, done = basic(store) + enable(store) + reviewer = Reviewer(store, cite('completed')) + before = row(store, gid) + clock = Clock() + engine, rid = run_goals(store, reviewer, clock) + call, = reviewer.calls + assert call['purpose'] == AUTO_REVIEW_PURPOSE and call['max_tokens'] == 16000 + assert call['system'] == auto_review_prompt() + payload = call['payload'] + assert [m['id'] for m in payload['sources']] == [source] + assert [m['id'] for m in payload['messages']] == [done] + assert payload['goal']['people'] == ['P1'] and payload['people'][0]['name'] == '程栗' + after = row(store, gid) + assert (after['state'], after['closed_by'], after['closed_at']) == ('completed', 'maintenance', clock().isoformat()) + assert after['revision'] == before['revision'] + 1 and after['content'] == before['content'] + record, = reviews(store, gid) + assert (record['outcome'], record['decision'], record['reason']) == ('closed', 'completed', 'auto_completed') + assert json.loads(record['evidence_json']) == [done] and record['revision_after'] == after['revision'] + assert record['latest_message_id'] == done and record['prompt_version'] == 'goal_review_auto_v1' + with store.read() as conn: + snapshot = dict(conn.execute("SELECT * FROM goal_revisions WHERE goal_id=? AND action='auto_review'", (gid,)).fetchone()) + operation = dict(conn.execute("SELECT * FROM admin_operations WHERE action='goal_auto_review'").fetchone()) + assert (snapshot['actor'], snapshot['reason']) == ('maintenance', 'auto_completed') + assert json.loads(snapshot['after_json'])['state'] == 'completed' + assert operation['actor'] == 'maintenance' and json.loads(operation['details_json'])['evidence'] == [done] + item, = items(store, rid) + assert (item['outcome'], item['reason'], item['object_id']) == ('goals_auto_completed', 'auto_completed', gid) + assert item['details']['evidence'] == [done] + report = engine.report(rid) + assert report['goal_auto_review']['enabled'] is True and report['goal_auto_review']['calls'] == 1 + assert report['summary']['goals_auto_completed']['object_ids'] == [gid] + + +def test_explicit_cancellation_abandons(store): + gid, _, done = basic(store) + enable(store) + run_goals(store, Reviewer(store, cite('abandoned'))) + assert row(store, gid)['state'] == 'abandoned' + assert reviews(store, gid)[0]['reason'] == 'auto_abandoned' + + +def _kept(store, gid, reason): + record, = reviews(store, gid) + assert (record['outcome'], record['reason']) == ('kept_open', reason) + assert row(store, gid)['state'] == 'open' and row(store, gid)['closed_by'] is None + + +@pytest.mark.parametrize('answer,reason', [ + ({'decision': 'open', 'evidence': [], 'reason': '还没做完'}, 'decision_open'), + ({'decision': 'completed', 'evidence': [], 'reason': ''}, 'evidence_empty'), + ({'decision': 'completed', 'evidence': [987654], 'reason': ''}, 'evidence_outside_material'), + ({'decision': 'completed', 'evidence': ['1'], 'reason': ''}, 'invalid_output'), + ({'decision': 'done', 'evidence': [], 'reason': ''}, 'invalid_output'), + ({'decision': 'completed', 'evidence': [], 'note': 'extra'}, 'invalid_output'), +]) +def test_rule_one_failures_keep_goal_open(store, answer, reason): + gid, _, _ = basic(store) + enable(store) + run_goals(store, Reviewer(store, answer)) + _kept(store, gid, reason) + + +def test_unparseable_answer_after_repair_keeps_goal_open(store): + gid, _, _ = basic(store) + enable(store) + run_goals(store, Reviewer(store, ModelError('retryable', 'JSON parse failed after repair', 'not json'))) + _kept(store, gid, 'invalid_output') + + +def test_own_source_is_never_evidence_but_a_later_candidate_alongside_it_is(store): + gid, source, done = basic(store) + enable(store) + run_goals(store, Reviewer(store, cite('completed', lambda payload: [payload['sources'][0]['id']]))) + _kept(store, gid, 'evidence_only_sources') + # Passing direction: one later qualifying candidate among the evidence suffices. + message(store, '还有个事', at='2026-11-02T20:00:00+08:00') + run_goals(store, Reviewer(store, cite('completed', lambda payload: [source, done]))) + assert row(store, gid)['state'] == 'completed' + record = reviews(store, gid)[-1] + with store.read() as conn: + protected = [r[0] for r in conn.execute('SELECT message_id FROM goal_auto_review_evidence WHERE review_id=?', + (record['id'],))] + assert protected == [done] + + +def test_sources_inherited_by_a_merge_are_not_evidence_even_when_later(store): + from iris.goals import _merge, _row + subject(store, 'p1', '程栗') + source = message(store, '能帮我取一下南门驿站的电热毯吗', at='2026-11-02T17:10:00+08:00') + gid = goal(store, '帮程栗取南门驿站的电热毯', sources=[source]) + intent = message(store, '我明天自己去驿站把电热毯拿回来', at='2026-11-02T18:00:00+08:00') + later = goal(store, '帮程栗取南门驿站电热毯', sources=[intent], created='2026-11-02T10:05:00+00:00') + with store.write() as conn: + _merge(conn, _row(conn, gid), _row(conn, later), Clock()(), actor='admin', semantic=True) + assert row(store, later)['merged_into'] == gid + chat = message(store, '今天好冷', at='2026-11-02T19:00:00+08:00') + enable(store) + reviewer = Reviewer(store, cite('completed', lambda payload: [intent])) + run_goals(store, reviewer) + payload = reviewer.calls[0]['payload'] + assert intent in [m['id'] for m in payload['sources']] and intent not in [m['id'] for m in payload['messages']] + _kept(store, gid, 'evidence_only_sources') + done = message(store, '电热毯拿回来了,放餐桌上了', at='2026-11-02T20:00:00+08:00') + run_goals(store, Reviewer(store, cite('completed', lambda payload: [intent, done]))) + assert row(store, gid)['state'] == 'completed' and chat + + +def _apply(store, gid, material, evidence): + from iris.goals import apply_goal_review + with store.write() as conn: + return apply_goal_review(conn, Clock()(), material=material, + result={'decision': 'completed', 'evidence': evidence}) + + +def test_code_checks_reject_early_and_uninvolved_evidence_even_in_a_crafted_material(store): + """Selection already excludes these; the write-time checks must hold on their own.""" + from iris.goals import auto_review_material + gid, source, done = basic(store) + early = message(store, '电热毯拿到了', at='2026-11-02T17:12:00+08:00') + proxy = message(store, '程栗的电热毯我替她拿了', sender='p2', at='2026-11-02T18:00:00+08:00') + with store.read() as conn: + material = auto_review_material(conn, gid, current=Clock()()) + assert early not in material['candidate_ids'] and proxy not in material['candidate_ids'] + for extra, reason in ((early, 'evidence_not_after_creation'), (proxy, 'evidence_sender_not_involved')): + crafted = {**material, 'message_ids': [*material['message_ids'], extra], + 'candidate_ids': [*material['candidate_ids'], extra], 'latest_message_id': extra + 1000} + assert _apply(store, gid, crafted, [extra])['reason'] == reason + assert row(store, gid)['state'] == 'open' + crafted = {**material, 'latest_message_id': material['latest_message_id'] + 2000} + assert _apply(store, gid, crafted, [done])['outcome'] == 'closed' + + +def test_time_is_compared_as_instants_not_local_strings(store): + subject(store, 'p1', '程栗') + # 17:12 in UTC+8 is before the 09:13 UTC creation even though "17:12" > "09:13". + early = message(store, '电热毯拿到了', at='2026-11-02T17:12:00+08:00') + gid = goal(store) + assert message(store, '随便聊聊', at='2026-11-02T17:14:00+08:00') + enable(store) + reviewer = Reviewer(store, cite('completed')) + run_goals(store, reviewer) + assert early not in [m['id'] for m in reviewer.calls[0]['payload']['messages']] + + +def test_uninvolved_sender_never_becomes_a_candidate(store): + subject(store, 'p1', '程栗') + subject(store, 'p2', '路人') + gid = goal(store, people=('p1',)) + proxy = message(store, '程栗那个快递我替她拿了', sender='p2', at='2026-11-02T18:00:00+08:00') + message(store, '今天好冷', at='2026-11-02T18:30:00+08:00') + enable(store) + reviewer = Reviewer(store, cite('completed', lambda payload: [proxy])) + run_goals(store, reviewer) + assert proxy not in [m['id'] for m in reviewer.calls[0]['payload']['messages']] + _kept(store, gid, 'evidence_outside_material') + own = message(store, '我自己拿到了', at='2026-11-02T19:00:00+08:00') + run_goals(store, Reviewer(store, cite('completed', lambda payload: [own]))) + assert row(store, gid)['state'] == 'completed' + + +def test_self_counts_as_involved(store): + gid, _, _ = basic(store) + mine = message(store, '电热毯取回来了,放在餐桌上', sender='self', kind='self_output', at='2026-11-02T19:04:00+08:00') + enable(store) + run_goals(store, Reviewer(store, cite('completed', lambda payload: [mine]))) + assert row(store, gid)['state'] == 'completed' + + +def test_rule_two_kind_failure_for_event_and_pass_for_action_result(store): + subject(store, 'p1', '程栗') + gid = goal(store) + event = message(store, '程栗将群待办标记为完成', kind='event', at='2026-11-02T18:00:00+08:00') + enable(store) + run_goals(store, Reviewer(store, cite('completed', lambda payload: [event]))) + _kept(store, gid, 'evidence_kind_not_allowed') + result = message(store, '取件成功', sender='self', kind='action_result', at='2026-11-02T18:30:00+08:00') + run_goals(store, Reviewer(store, cite('completed', lambda payload: [result]))) + assert row(store, gid)['state'] == 'completed' + + +def test_people_empty_falls_back_to_source_senders(store): + subject(store, 'p1', '程栗') + subject(store, 'p2', '路人') + source = message(store, '帮我记着周五交表', at='2026-11-02T17:00:00+08:00') + gid = goal(store, '提醒程栗周五交表', people=(), sources=[source]) + done = message(store, '表交了', at='2026-11-02T19:00:00+08:00') + other = message(store, '程栗表交了', sender='p2', at='2026-11-02T19:01:00+08:00') + enable(store) + reviewer = Reviewer(store, cite('completed', lambda payload: [done])) + run_goals(store, reviewer) + ids = [m['id'] for m in reviewer.calls[0]['payload']['messages']] + assert done in ids and other not in ids + assert row(store, gid)['state'] == 'completed' + + +@pytest.mark.parametrize('change', ['edit', 'complete', 'merge_origin']) +def test_rule_three_goal_changed_during_call_skips_without_fingerprint(store, change): + gid, _, _ = basic(store) + enable(store) + goals = Goals(store, clock=Clock()) + + def hook(payload): + current = row(store, gid) + if change == 'edit': + goals.update(gid, expected_revision=current['revision'], content='帮程栗取两件快递', actor='admin') + elif change == 'complete': + goals.update(gid, expected_revision=current['revision'], state='completed', actor='admin') + else: + with store.write() as conn: + conn.execute("UPDATE goals SET origin='host' WHERE id=?", (gid,)) + reviewer = Reviewer(store, cite('completed'), hook=hook) + engine, rid = run_goals(store, reviewer) + assert len(reviewer.calls) == 1 and reviews(store) == [] + assert engine.report(rid)['goal_auto_review']['revision_conflict'] == 1 + if change == 'edit': + assert row(store, gid)['state'] == 'open' + reviewer.hook = None + run_goals(store, reviewer) # New revision, new fingerprint: judged again. + assert len(reviewer.calls) == 2 and row(store, gid)['state'] == 'completed' + elif change == 'complete': + assert row(store, gid)['closed_by'] == 'admin' + else: + assert row(store, gid)['state'] == 'open' + + +def test_fingerprint_prevents_repeat_calls_until_a_new_message(store): + gid, _, _ = basic(store) + enable(store) + reviewer = Reviewer(store, {'decision': 'open', 'evidence': [], 'reason': '未完成'}) + run_goals(store, reviewer) + engine, rid = run_goals(store, reviewer) + assert len(reviewer.calls) == 1 + assert engine.report(rid)['goal_auto_review']['already_judged'] == 1 + message(store, '电热毯拿到啦', at='2026-11-02T21:00:00+08:00') + run_goals(store, reviewer) + assert len(reviewer.calls) == 2 and len(reviews(store, gid)) == 2 + + +def test_interrupted_run_resumes_without_reprocessing(store): + enable(store) + subject(store, 'p1', '程栗') + first = goal(store, '帮程栗取快递') + second = goal(store, '帮程栗交水费') + message(store, '快递和水费都弄好了', at='2026-11-02T19:00:00+08:00') + stopped = [] + reviewer = Reviewer(store, cite('completed'), hook=lambda payload: stopped.append(True)) + engine, rid = run_goals(store, reviewer, stop=lambda: bool(stopped)) + assert len(reviewer.calls) == 1 and row(store, first)['state'] == 'completed' + with store.read() as conn: + assert conn.execute('SELECT state FROM maintenance_runs WHERE id=?', (rid,)).fetchone()[0] == 'running' + reviewer.hook = None + engine.run(rid) + engine.run(rid) + assert len(reviewer.calls) == 2 and [c['payload']['goal']['text'] for c in reviewer.calls] == ['帮程栗取快递', '帮程栗交水费'] + assert row(store, second)['state'] == 'completed' + assert len(reviews(store)) == 2 + + +def test_model_failure_is_retried_later_without_fingerprint(store): + gid, _, _ = basic(store) + enable(store) + reviewer = Reviewer(store, ModelError('retryable', 'total timeout', reason='timeout')) + engine, rid = run_goals(store, reviewer) + assert reviews(store) == [] and row(store, gid)['state'] == 'open' + item, = items(store, rid) + assert (item['outcome'], item['reason']) == ('failed', 'timeout') + reviewer.answer = cite('completed') + run_goals(store, reviewer) + assert len(reviewer.calls) == 2 and row(store, gid)['state'] == 'completed' + + +def test_paused_purpose_stops_the_pass(store): + enable(store) + subject(store, 'p1', '程栗') + for text in ('帮程栗取快递', '帮程栗交水费'): + goal(store, text) + message(store, '都好了', at='2026-11-02T19:00:00+08:00') + reviewer = Reviewer(store, ModelError('paused', 'paused', paused=True, reason='paused')) + engine, rid = run_goals(store, reviewer) + assert len(reviewer.calls) == 1 + assert engine.report(rid)['goal_auto_review']['stopped'] == 'paused' + + +@pytest.mark.parametrize('origin,kind', [('host', 'normal'), ('admin', 'normal'), ('internal', 'question')]) +def test_host_admin_and_questions_never_enter_material(store, origin, kind): + subject(store, 'p1', '程栗') + gid = goal(store, origin=origin, kind=kind) + message(store, '电热毯拿到了,搞定', at='2026-11-02T19:00:00+08:00') + enable(store) + reviewer = Reviewer(store, cite('completed')) + run_goals(store, reviewer) + assert reviewer.calls == [] and row(store, gid)['state'] == 'open' + + +def test_internal_goal_that_absorbed_a_host_goal_is_not_reviewed(store): + subject(store, 'p1', '程栗') + gid = goal(store) + absorbed = goal(store, origin='host') + with store.write() as conn: + conn.execute('UPDATE goals SET merged_into=? WHERE id=?', (gid, absorbed)) + message(store, '电热毯拿到了', at='2026-11-02T19:00:00+08:00') + enable(store) + reviewer = Reviewer(store, cite('completed')) + run_goals(store, reviewer) + assert reviewer.calls == [] and row(store, gid)['state'] == 'open' + + +def test_no_candidate_message_means_no_model_call(store): + subject(store, 'p1', '程栗') + subject(store, 'p2', '路人') + gid = goal(store) + message(store, '早就拿到了', at='2026-11-02T16:00:00+08:00') # before creation + message(store, '程栗拿到了', sender='p2', at='2026-11-02T19:00:00+08:00') # not involved + enable(store) + reviewer = Reviewer(store, cite('completed')) + engine, rid = run_goals(store, reviewer) + assert reviewer.calls == [] and engine.report(rid)['goal_auto_review']['no_candidates'] == 1 + + +def test_candidates_prefer_goal_entry_and_keywords_then_recent_at_most_30(store): + subject(store, 'p1', '程栗') + gid = goal(store, '帮程栗取南门驿站的电热毯快递') + for minute in range(40): + message(store, f'闲聊{minute}', entry_id='e2', at=f'2026-11-02T18:{minute:02d}:00+08:00') + keyword = message(store, '电热毯还在驿站吗', entry_id='e2', at='2026-11-02T17:20:00+08:00') + home = message(store, '今天风好大', at='2026-11-02T17:30:00+08:00') + enable(store) + reviewer = Reviewer(store, {'decision': 'open', 'evidence': [], 'reason': ''}) + run_goals(store, reviewer) + picked = [m['id'] for m in reviewer.calls[0]['payload']['messages']] + assert len(picked) == 30 and keyword in picked and home in picked + times = [m['at'] for m in reviewer.calls[0]['payload']['messages']] + assert times == sorted(times) # chronological in the material + + +def test_messages_from_more_restricted_entries_are_not_candidates(store): + subject(store, 'p1', '程栗') + gid = goal(store) + with store.write() as conn: + entry(conn, 'secret', visibility='entry_only') + hidden = message(store, '电热毯拿到了', entry_id='secret', at='2026-11-02T19:00:00+08:00') + shown = message(store, '在路上', at='2026-11-02T19:10:00+08:00') + enable(store) + reviewer = Reviewer(store, {'decision': 'open', 'evidence': [], 'reason': ''}) + run_goals(store, reviewer) + picked = [m['id'] for m in reviewer.calls[0]['payload']['messages']] + assert hidden not in picked and shown in picked + + +def test_closing_cancels_untaken_reminders(store): + subject(store, 'p1', '程栗') + source = message(store, '帮我取一下快递', at='2026-11-02T17:00:00+08:00') + goals = Goals(store, clock=Clock(CREATED)) + store.set_setting('goal_dedup_judge', {'enabled': False}) + with store.write() as conn: + from iris.goals import write_learning_goal + gid = write_learning_goal(conn, content='帮程栗取快递', kind='normal', deadline='2026-11-02T20:00:00+08:00', + entry_id='e1', evidence=[source], current=Clock(CREATED)())['submitted_id'] + Goals(store, clock=Clock()).generate_notifications() + with store.read() as conn: + assert conn.execute("SELECT COUNT(*) FROM notifications WHERE goal_id=? AND status='pending'", (gid,)).fetchone()[0] + message(store, '快递拿到了', at='2026-11-02T19:00:00+08:00') + enable(store) + run_goals(store, Reviewer(store, cite('completed'))) + with store.read() as conn: + assert conn.execute("SELECT COUNT(*) FROM notifications WHERE goal_id=? AND status='pending'", (gid,)).fetchone()[0] == 0 + assert conn.execute("SELECT COUNT(*) FROM goal_reminder_plans WHERE goal_id=? AND status='scheduled'", (gid,)).fetchone()[0] == 0 + assert goals.get(gid)['state'] == 'completed' + + +def test_reopen_sticks_until_new_evidence_then_can_be_reviewed_again(store): + gid, _, _ = basic(store) + enable(store) + reviewer = Reviewer(store, cite('completed')) + run_goals(store, reviewer) + closed = row(store, gid) + goals = Goals(store, clock=Clock()) + with pytest.raises(GoalConflict): + goals.reopen(gid, expected_revision=closed['revision'] - 1) + reopened = goals.reopen(gid, expected_revision=closed['revision']) + assert reopened['state'] == 'open' and reopened['closed_by'] is None + assert reopened['revision'] == closed['revision'] + 1 + with store.read() as conn: + snapshot = dict(conn.execute("SELECT * FROM goal_revisions WHERE goal_id=? AND action='reopen'", (gid,)).fetchone()) + assert json.loads(snapshot['after_json'])['state'] == 'open' and snapshot['actor'] == 'admin' + engine, rid = run_goals(store, reviewer) + assert len(reviewer.calls) == 1 and engine.report(rid)['goal_auto_review']['no_candidates'] == 1 + assert row(store, gid)['state'] == 'open' + later = message(store, '这次真的收到了', at='2026-11-03T10:00:00+08:00') + run_goals(store, reviewer) + assert len(reviewer.calls) == 2 and [m['id'] for m in reviewer.calls[1]['payload']['messages']] == [later] + assert row(store, gid)['state'] == 'completed' + + +def test_only_automatic_closures_can_be_reopened(store): + gid, _, _ = basic(store) + goals = Goals(store, clock=Clock()) + closed = goals.update(gid, expected_revision=row(store, gid)['revision'], state='completed', actor='admin') + with pytest.raises(GoalError): + goals.reopen(gid, expected_revision=closed['revision']) + + +def test_crash_after_commit_is_not_judged_again_and_crash_before_commit_is_retried(tmp_path, monkeypatch): + from iris.db import Store + from iris import goals as goals_module + from iris.maintenance import Maintenance + path = tmp_path / 'crash.db' + store = Store(path) + try: + enable(store) + subject(store, 'p1', '程栗') + first = goal(store, '帮程栗取快递') + second = goal(store, '帮程栗交水费') + message(store, '快递和水费都弄好了', at='2026-11-02T19:00:00+08:00') + original = goals_module.apply_goal_review + calls = [] + + def crash(conn, current, **kwargs): + calls.append(kwargs['material']['goal_id']) + if len(calls) == 1: + return original(conn, current, **kwargs) # committed, then the process dies + raise KeyboardInterrupt() # dies inside the second write: rolled back + monkeypatch.setattr(goals_module, 'apply_goal_review', crash) + reviewer = Reviewer(store, cite('completed')) + with pytest.raises(KeyboardInterrupt): + run_goals(store, reviewer) + assert row(store, first)['state'] == 'completed' and row(store, second)['state'] == 'open' + assert [r['goal_id'] for r in reviews(store)] == [first] + with store.read() as conn: + rid = conn.execute("SELECT id FROM maintenance_runs WHERE state='running'").fetchone()[0] + finally: + store.close() + monkeypatch.undo() + restarted = Store(path) + try: + resumed = Reviewer(restarted, cite('completed')) + Maintenance(restarted, gateway=resumed, clock=Clock()).run(rid) + assert [c['payload']['goal']['text'] for c in resumed.calls] == ['帮程栗交水费'] + assert row(restarted, second)['state'] == 'completed' + assert len(reviews(restarted)) == 2 + finally: + restarted.close() + + +def test_switch_turned_off_after_the_run_was_accepted_stops_calls(store): + gid, _, _ = basic(store) + enable(store) + store.set_setting('consolidation', {'enabled': False}) + store.set_setting('scheduled_backup', {'enabled': False}) + from iris.maintenance import Maintenance, PHASES + reviewer = Reviewer(store, cite('completed')) + engine = Maintenance(store, gateway=reviewer, clock=Clock()) + rid = engine.request() + enable(store, review=False) + with store.write() as conn: + conn.execute('UPDATE maintenance_runs SET phase=? WHERE id=?', (PHASES.index('goals'), rid)) + engine.run(rid) + assert reviewer.calls == [] and row(store, gid)['state'] == 'open' + assert engine.report(rid)['goal_auto_review'] == {'enabled': True, 'stopped': 'disabled'} + + +def test_newest_qualified_messages_are_kept_when_priority_messages_fill_the_material(store): + subject(store, 'p1', '程栗') + gid = goal(store, '帮程栗取南门驿站的电热毯快递') + for minute in range(35): + message(store, f'电热毯快递的进展{minute}', at=f'2026-11-02T18:{minute:02d}:00+08:00') + retraction = message(store, '刚才说错了,还没弄完', entry_id='e2', at='2026-11-02T19:30:00+08:00') + enable(store) + reviewer = Reviewer(store, {'decision': 'open', 'evidence': [], 'reason': ''}) + run_goals(store, reviewer) + picked = [m['id'] for m in reviewer.calls[0]['payload']['messages']] + assert len(picked) == 30 and retraction in picked + # A newer qualified message changes the fingerprint and is judged again. + run_goals(store, reviewer) + assert len(reviewer.calls) == 1 + message(store, '这次真的弄完了', entry_id='e3', at='2026-11-02T20:00:00+08:00') + run_goals(store, reviewer) + assert len(reviewer.calls) == 2 + + +def test_priority_order_still_fills_beyond_the_newest_ten(store): + subject(store, 'p1', '程栗') + gid = goal(store, '帮程栗取南门驿站的电热毯快递') + keyword = message(store, '电热毯还在驿站', entry_id='e2', at='2026-11-02T17:20:00+08:00') + for minute in range(25): + message(store, f'闲聊{minute}', entry_id='e3', at=f'2026-11-02T18:{minute:02d}:00+08:00') + enable(store) + reviewer = Reviewer(store, {'decision': 'open', 'evidence': [], 'reason': ''}) + run_goals(store, reviewer) + picked = [m['id'] for m in reviewer.calls[0]['payload']['messages']] + assert len(picked) == 26 and keyword in picked + assert gid + + +def _gateway(store, clock, hits, *, limit_used): + import httpx + from iris.models import Gateway, ModelConfig + from iris.model_health import ModelHealth + + def respond(request): + hits.append(request.url.path) + body = json.loads(request.content) + material = json.loads(body['messages'][-1]['content']) + if 'incoming' in material: + content = json.dumps({'decisions': [{'id': c['id'], 'verdict': 'same'} for c in material['candidates']]}) + else: + content = json.dumps({'decision': 'completed', 'evidence': [material['messages'][-1]['id']], 'reason': 'x'}) + return httpx.Response(200, json={'choices': [{'message': {'content': content}, 'finish_reason': 'stop'}], + 'usage': {'prompt_tokens': 1, 'completion_tokens': 1}}) + configs = {'chat': ModelConfig('https://g-c1.invalid/v1', '', 'unit')} + health = ModelHealth(store, configs, clock=clock) + health.set_daily_token_limit(10) + if limit_used: + with store.write() as conn: + conn.execute("""INSERT INTO model_calls(purpose,model,duration_ms,prompt_tokens,completion_tokens, + result_category,created_at) VALUES('learning','unit',1,1000,1000,'success',?)""", (clock().isoformat(),)) + client = httpx.Client(transport=httpx.MockTransport(respond)) + return Gateway(configs, store, health=health, clock=clock, client=client, sleeper=lambda s: None), client + + +@pytest.mark.parametrize('limit_used', [True, False]) +def test_daily_token_limit_stops_g1_without_requests_or_fingerprints(store, limit_used): + gid, _, _ = basic(store) + enable(store) + clock, hits = Clock(), [] + gateway, client = _gateway(store, clock, hits, limit_used=limit_used) + try: + engine, rid = run_goals(store, gateway, clock) + finally: + gateway.close() + client.close() + if limit_used: + assert hits == [] and row(store, gid)['state'] == 'open' and reviews(store) == [] + summary = engine.report(rid)['goal_auto_review'] + assert summary['stopped'] == 'usage_limit' and summary['calls'] == 0 + assert [i for i in items(store, rid) if i['outcome'] == 'failed'] == [] + else: + assert len(hits) == 1 and row(store, gid)['state'] == 'completed' + + +@pytest.mark.parametrize('limit_used', [True, False]) +def test_daily_token_limit_also_stops_g2(store, limit_used): + subject(store, 'p1', '程栗') + first = goal(store, '周五前整理操作手册', people=(), created='2026-11-02T09:00:00+00:00') + second = goal(store, '周五前整理操作说明手册', people=(), created='2026-11-02T10:00:00+00:00') + store.set_setting('goal_dedup_judge', {'enabled': True, 'budget_seconds': 1}) + with store.write() as conn: + conn.execute("INSERT INTO goal_duplicates(goal_a,goal_b,status,created_at) VALUES(?,?,'possible',?)", + (first, second, CREATED)) + enable(store, review=False, duplicates=True) + clock, hits = Clock(), [] + gateway, client = _gateway(store, clock, hits, limit_used=limit_used) + try: + engine, rid = run_goals(store, gateway, clock) + finally: + gateway.close() + client.close() + if limit_used: + assert hits == [] and row(store, second)['merged_into'] is None + assert engine.report(rid)['goal_duplicate_review']['stopped'] == 'usage_limit' + else: + assert len(hits) == 1 and row(store, second)['merged_into'] == first + + +@pytest.mark.parametrize('reopened', [True, False]) +def test_merge_keeps_the_reopen_watermark(store, reopened): + """G1 closes X, the administrator reopens X, G2 merges X into Y: Y keeps X's rejection.""" + gid, source, done = basic(store) + enable(store) + run_goals(store, Reviewer(store, cite('completed', lambda payload: [done]))) + if reopened: + Goals(store, clock=Clock()).reopen(gid, expected_revision=row(store, gid)['revision']) + else: + with store.write() as conn: # Same goal reopened without a watermark (control). + conn.execute("UPDATE goals SET state='open',closed_at=NULL,closed_by=NULL,revision=revision+1 WHERE id=?", (gid,)) + earlier = goal(store, '帮程栗取南门驿站电热毯的快递', created='2026-11-02T09:00:00+00:00') + store.set_setting('goal_dedup_judge', {'enabled': True, 'budget_seconds': 1}) + with store.write() as conn: + conn.execute("INSERT INTO goal_duplicates(goal_a,goal_b,status,created_at) VALUES(?,?,'possible',?)", + (min(earlier, gid), max(earlier, gid), CREATED)) + + class Both(Reviewer): + def goal_dedup_judge(self, messages, ids, *, budget_seconds): + return {'decisions': {i: 'same' for i in ids}} + enable(store, review=True, duplicates=True) + reviewer = Both(store, cite('completed', lambda payload: [done])) + run_goals(store, reviewer) + assert row(store, gid)['merged_into'] == earlier + # The survivor of this run's merge is left alone until the next run. + assert row(store, earlier)['state'] == 'open' and reviewer.calls == [] + run_goals(store, reviewer, Clock('2026-11-04T01:00:00+00:00')) + if reopened: + assert row(store, earlier)['auto_review_after_message_id'] == row(store, gid)['auto_review_after_message_id'] + assert row(store, earlier)['state'] == 'open' and reviewer.calls == [] + else: + assert row(store, earlier)['state'] == 'completed' + + +def test_closure_evidence_is_protected_from_message_retention(store): + from iris.memory_ops import message_references + gid, source, done = basic(store) + chat = message(store, '今天好冷', at='2026-11-02T19:30:00+08:00') + enable(store) + run_goals(store, Reviewer(store, cite('completed', lambda payload: [done]))) + with store.read() as conn: + assert 'goal_auto_evidence' in message_references(conn, done) + assert message_references(conn, chat) == [] + # Evidence of a judgment that kept the goal open protects nothing. + other = goal(store, '帮程栗交水费') + note = message(store, '水费的事回头再说', at='2026-11-02T20:00:00+08:00') + run_goals(store, Reviewer(store, {'decision': 'open', 'evidence': [note], 'reason': ''})) + assert reviews(store, other)[0]['outcome'] == 'kept_open' + with store.read() as conn: + assert 'goal_auto_evidence' not in message_references(conn, note) + # The daily message cleanup keeps the protected message and removes the other old one. + from iris.maintenance import Maintenance + store.set_setting('lifecycle', {'message_retention_days': 1}) + late = Clock('2026-12-20T01:00:00+00:00') + engine = Maintenance(store, gateway=None, clock=late) + rid = engine.request() + engine.run(rid) + with store.read() as conn: + left = {r[0] for r in conn.execute('SELECT id FROM messages')} + assert done in left and chat not in left + + +def test_report_counts_accumulate_across_an_interrupted_run(store): + enable(store) + subject(store, 'p1', '程栗') + first = goal(store, '帮程栗取快递') + second = goal(store, '帮程栗交水费') + message(store, '快递和水费都弄好了', at='2026-11-02T19:00:00+08:00') + stopped = [] + reviewer = Reviewer(store, cite('completed'), hook=lambda payload: stopped.append(True)) + engine, rid = run_goals(store, reviewer, stop=lambda: bool(stopped)) + assert engine.report(rid)['goal_auto_review']['calls'] == 1 + reviewer.hook = None + engine.run(rid) + summary = engine.report(rid)['goal_auto_review'] + assert (summary['goals'], summary['calls'], summary['closed']) == (2, 2, 2) + assert 'already_judged' not in summary and 'cursor' not in summary + assert row(store, first)['state'] == row(store, second)['state'] == 'completed' + + +def test_call_cap_reports_budget_exhausted_and_the_rest_waits(store, monkeypatch): + from iris import goals as goals_module + monkeypatch.setattr(goals_module, 'AUTO_REVIEW_MAX_CALLS', 1) + enable(store) + subject(store, 'p1', '程栗') + first = goal(store, '帮程栗取快递') + second = goal(store, '帮程栗交水费') + message(store, '快递和水费都弄好了', at='2026-11-02T19:00:00+08:00') + reviewer = Reviewer(store, cite('completed')) + engine, rid = run_goals(store, reviewer) + summary = engine.report(rid)['goal_auto_review'] + assert (summary['calls'], summary['budget_exhausted']) == (1, 1) + assert row(store, first)['state'] == 'completed' and row(store, second)['state'] == 'open' + run_goals(store, reviewer) + assert row(store, second)['state'] == 'completed' + + +def test_role_messages_in_unrelated_entries_do_not_cause_daily_rejudgment(store): + from datetime import datetime, timedelta + subject(store, 'p1', '程栗') + gid = goal(store, '帮程栗取快递') + message(store, '在吗', at='2026-11-02T18:00:00+08:00') + unrelated = message(store, '好的,收到', sender='self', kind='self_output', entry_id='other', + at='2026-11-02T18:30:00+08:00') + enable(store) + reviewer = Reviewer(store, {'decision': 'open', 'evidence': [], 'reason': ''}) + day = datetime.fromisoformat('2026-11-03T01:00:00+00:00') + run_goals(store, reviewer, Clock(day.isoformat())) + assert unrelated not in [m['id'] for m in reviewer.calls[0]['payload']['messages']] + for n in range(1, 4): # the role speaks once a day in an unrelated entry only + message(store, '好的,收到', sender='self', kind='self_output', entry_id='other', + at=(day + timedelta(days=n - 1, hours=2)).isoformat()) + run_goals(store, reviewer, Clock((day + timedelta(days=n)).isoformat())) + assert len(reviewer.calls) == 1 + # Passing direction: the role's reply in the goal's own conversation is a candidate. + message(store, '快递我拿到了', sender='self', kind='self_output', entry_id='e1', + at=(day + timedelta(days=4)).isoformat()) + run_goals(store, reviewer, Clock((day + timedelta(days=5)).isoformat())) + assert len(reviewer.calls) == 2 and gid + + +def test_role_messages_count_where_the_goal_people_speak(store): + subject(store, 'p1', '程栗') + gid = goal(store, '帮程栗取快递') + message(store, '在群里也问一下', entry_id='group', at='2026-11-02T18:00:00+08:00') + there = message(store, '快递我拿到了', sender='self', kind='self_output', entry_id='group', + at='2026-11-02T18:10:00+08:00') + elsewhere = message(store, '快递我拿到了', sender='self', kind='self_output', entry_id='other', + at='2026-11-02T18:20:00+08:00') + enable(store) + reviewer = Reviewer(store, {'decision': 'open', 'evidence': [], 'reason': ''}) + run_goals(store, reviewer) + picked = [m['id'] for m in reviewer.calls[0]['payload']['messages']] + assert there in picked and elsewhere not in picked and gid + + +def test_call_cap_leftover_goal_is_first_in_line_next_time(store, monkeypatch): + from datetime import datetime, timedelta + from iris import goals as goals_module + monkeypatch.setattr(goals_module, 'AUTO_REVIEW_MAX_CALLS', 2) + for sid, name in (('p1', '程栗'), ('p2', '路人'), ('p3', '老周')): + subject(store, sid, name) + texts = ['帮程栗取快递', '帮路人交水费', '提醒老周买药'] + for text, sid in zip(texts, ('p1', 'p2', 'p3')): + goal(store, text, people=(sid,)) + message(store, '在吗', sender=sid, at='2026-11-02T18:00:00+08:00') + enable(store) + reviewer = Reviewer(store, {'decision': 'open', 'evidence': [], 'reason': ''}) + day = datetime.fromisoformat('2026-11-03T01:00:00+00:00') + engine, rid = run_goals(store, reviewer, Clock(day.isoformat())) + assert [c['payload']['goal']['text'] for c in reviewer.calls] == texts[:2] + assert engine.report(rid)['goal_auto_review']['budget_exhausted'] == 1 + run_goals(store, reviewer, Clock((day + timedelta(days=1)).isoformat())) + assert [c['payload']['goal']['text'] for c in reviewer.calls] == texts + # Once all have a judgment, the least recently reviewed go first; new evidence every day. + for n, expected in ((2, [texts[0], texts[1]]), (3, [texts[2], texts[0]])): + for sid in ('p1', 'p2', 'p3'): + message(store, '还在弄', sender=sid, at=(day + timedelta(days=n - 1, hours=12)).isoformat()) + before = len(reviewer.calls) + run_goals(store, reviewer, Clock((day + timedelta(days=n)).isoformat())) + assert [c['payload']['goal']['text'] for c in reviewer.calls[before:]] == expected + + +def test_goal_merged_into_this_run_is_reviewed_next_run(store): + gid, source, done = basic(store) + earlier = goal(store, '帮程栗取南门驿站电热毯的快递', created='2026-11-02T09:00:00+00:00') + store.set_setting('goal_dedup_judge', {'enabled': True, 'budget_seconds': 1}) + with store.write() as conn: + conn.execute("INSERT INTO goal_duplicates(goal_a,goal_b,status,created_at) VALUES(?,?,'possible',?)", + (min(earlier, gid), max(earlier, gid), CREATED)) + + class Both(Reviewer): + def goal_dedup_judge(self, messages, ids, *, budget_seconds): + return {'decisions': {i: 'same' for i in ids}} + enable(store, review=True, duplicates=True) + reviewer = Both(store, cite('completed', lambda payload: [done])) + engine, rid = run_goals(store, reviewer) + assert row(store, gid)['merged_into'] == earlier and reviewer.calls == [] + assert row(store, earlier)['state'] == 'open' + assert engine.report(rid)['goal_auto_review']['merged_this_run'] == 1 + run_goals(store, reviewer, Clock('2026-11-04T01:00:00+00:00')) + assert len(reviewer.calls) == 1 and row(store, earlier)['state'] == 'completed' + + +def test_reopen_releases_evidence_and_unqualified_co_citations_are_never_protected(store): + from iris.memory_ops import message_references + gid, source, done = basic(store) + event = message(store, '程栗把待办标成完成', kind='event', at='2026-11-02T19:10:00+08:00') + enable(store) + run_goals(store, Reviewer(store, cite('completed', lambda payload: [done, event, source]))) + assert row(store, gid)['state'] == 'completed' + with store.read() as conn: + assert 'goal_auto_evidence' in message_references(conn, done) + assert 'goal_auto_evidence' not in message_references(conn, event) + Goals(store, clock=Clock()).reopen(gid, expected_revision=row(store, gid)['revision']) + with store.read() as conn: + assert 'goal_auto_evidence' not in message_references(conn, done) + from iris.maintenance import Maintenance + store.set_setting('lifecycle', {'message_retention_days': 1}) + engine = Maintenance(store, gateway=None, clock=Clock('2026-12-20T01:00:00+00:00')) + engine.run(engine.request()) + with store.read() as conn: + left = {r[0] for r in conn.execute('SELECT id FROM messages')} + assert done not in left and source in left # the goal source stays protected as before + + + +def test_people_speaking_in_a_group_only_before_creation_do_not_widen_the_conversation(store): + subject(store, 'p1', '程栗') + message(store, '在群里说一下', entry_id='group', at='2026-11-02T16:00:00+08:00') # before creation + gid = goal(store, '帮程栗取快递') + message(store, '在吗', at='2026-11-02T18:00:00+08:00') + mine = message(store, '快递我拿到了', sender='self', kind='self_output', entry_id='group', + at='2026-11-02T18:10:00+08:00') + enable(store) + reviewer = Reviewer(store, {'decision': 'open', 'evidence': [], 'reason': ''}) + run_goals(store, reviewer) + assert mine not in [m['id'] for m in reviewer.calls[0]['payload']['messages']] + message(store, '好的', sender='self', kind='self_output', entry_id='group', at='2026-11-02T19:00:00+08:00') + run_goals(store, reviewer, Clock('2026-11-04T01:00:00+00:00')) + assert len(reviewer.calls) == 1 # no new fingerprint from that group + # Passing direction: once the person speaks there after creation, the group counts. + message(store, '群里也说一声', entry_id='group', at='2026-11-02T20:00:00+08:00') + run_goals(store, reviewer, Clock('2026-11-05T01:00:00+00:00')) + assert len(reviewer.calls) == 2 and mine in [m['id'] for m in reviewer.calls[1]['payload']['messages']] + assert gid diff --git a/tests/test_goal_auto_review_api.py b/tests/test_goal_auto_review_api.py new file mode 100644 index 00000000..ab95a0f1 --- /dev/null +++ b/tests/test_goal_auto_review_api.py @@ -0,0 +1,116 @@ +"""Admin surface of the G line: two switches, auto actions, evidence and reopen.""" +import json + +import pytest +from fastapi.testclient import TestClient + +from conftest import authorize_host, login_admin +from iris.api import create_app +from iris.goals import AUTO_REVIEW_KEY, DUPLICATE_AUTO_KEY, Goals +from goal_auto_helpers import Clock, Reviewer, basic, cite, enable, row, run_goals + +PATH = '/admin/api/settings/goals' + + +@pytest.fixture +def client(store): + store.set_setting('goal_dedup_judge', {'enabled': False}) + with TestClient(create_app(store=store, configs={}), base_url='http://127.0.0.1', + client=('127.0.0.1', 1000)) as client: + client.app.state.scheduler.stop() + client.app.state.goals = Goals(store, clock=Clock()) + login_admin(client) + yield client + + +def test_switches_are_off_by_default_and_saved_only_when_set(client, store): + goals = client.get('/admin/api/settings').json()['goals'] + assert goals[AUTO_REVIEW_KEY] is False and goals[DUPLICATE_AUTO_KEY] is False + response = client.patch(PATH, json={'default_reminder_minutes': 30}) + assert response.status_code == 200 + assert AUTO_REVIEW_KEY not in store.setting('goals') and DUPLICATE_AUTO_KEY not in store.setting('goals') + response = client.patch(PATH, json={AUTO_REVIEW_KEY: True}) + assert response.status_code == 200 and response.json()['goals'][AUTO_REVIEW_KEY] is True + assert response.json()['goals'][DUPLICATE_AUTO_KEY] is False + assert store.setting('goals')[AUTO_REVIEW_KEY] is True and DUPLICATE_AUTO_KEY not in store.setting('goals') + response = client.patch(PATH, json={DUPLICATE_AUTO_KEY: True, AUTO_REVIEW_KEY: False}) + assert response.json()['goals'][DUPLICATE_AUTO_KEY] is True and response.json()['goals'][AUTO_REVIEW_KEY] is False + with store.read() as conn: + audit = [json.loads(r[0]) for r in conn.execute( + "SELECT details_json FROM admin_operations WHERE action='settings_goals' ORDER BY id")] + assert audit[-1] == {DUPLICATE_AUTO_KEY: True, AUTO_REVIEW_KEY: False} + + +@pytest.mark.parametrize('value', [1, 'true', None, []]) +def test_switches_are_strict_booleans(client, store, value): + before = store.setting('goals') + assert client.patch(PATH, json={AUTO_REVIEW_KEY: value}).status_code == 400 + assert store.setting('goals') == before + + +def test_detail_list_actions_and_reopen(client, store): + gid, source, done = basic(store) + enable(store) + run_goals(store, Reviewer(store, cite('completed'))) + detail = client.get(f'/admin/api/goals/{gid}').json() + assert detail['state'] == 'completed' and detail['closed_by'] == 'maintenance' and detail['auto_closed'] is True + closure = detail['auto_review']['closure'] + assert closure['decision'] == 'completed' and closure['evidence'] == [done] + assert closure['evidence_messages'][0]['content'] == '到家了,餐桌上的电热毯收到了,谢谢!' + assert detail['auto_review']['reopenable'] is True + listed = client.get('/admin/api/goals?state=completed').json()['items'] + assert [(g['id'], g['auto_closed']) for g in listed] == [(gid, True)] + actions = client.get('/admin/api/goal-auto-actions').json() + action, = actions['items'] + assert action['type'] == 'review' and action['goal']['id'] == gid and action['reopenable'] is True + assert action['evidence_messages'][0]['id'] == done + stale = client.post(f'/admin/api/goals/{gid}/reopen', json={'expected_revision': detail['revision'] - 1}) + assert stale.status_code == 409 + reopened = client.post(f'/admin/api/goals/{gid}/reopen', json={'expected_revision': detail['revision']}) + assert reopened.status_code == 200, reopened.text + assert reopened.json()['state'] == 'open' and row(store, gid)['closed_by'] is None + detail = client.get(f'/admin/api/goals/{gid}').json() + assert detail['auto_closed'] is False and detail['auto_review']['reopenable'] is False + assert client.get('/admin/api/goal-auto-actions').json()['items'][0]['reopenable'] is False + again = client.post(f'/admin/api/goals/{gid}/reopen', json={'expected_revision': detail['revision']}) + assert again.status_code == 400 + + +def test_host_projection_is_unchanged(client, store): + gid, _, _ = basic(store) + enable(store) + run_goals(store, Reviewer(store, cite('completed'))) + authorize_host(client) + item = client.get('/api/v1/goals?state=completed').json()['items'][0] + assert item['id'] == gid and 'auto_closed' not in item and 'auto_review' not in item + + +def test_undo_auto_merge_endpoint_lists_state_and_requires_csrf(client, store): + from goal_auto_helpers import CREATED, goal + first = goal(store, '周五前整理操作手册', people=(), created='2026-11-02T09:00:00+00:00') + second = goal(store, '周五前整理操作说明手册', people=(), created='2026-11-02T10:00:00+00:00') + with store.write() as conn: + conn.execute("INSERT INTO goal_duplicates(goal_a,goal_b,status,created_at) VALUES(?,?,'possible',?)", + (first, second, CREATED)) + store.set_setting('goal_dedup_judge', {'enabled': True, 'budget_seconds': 1}) + + class Same(Reviewer): + def goal_dedup_judge(self, messages, ids, *, budget_seconds): + return {'decisions': {i: 'same' for i in ids}} + enable(store, review=False, duplicates=True) + run_goals(store, Same(store)) + action, = client.get('/admin/api/goal-auto-actions').json()['items'] + assert action['type'] == 'duplicate' and action['outcome'] == 'merged' and action['undoable'] is True + assert action['source_id'] == second and 'merge_record_json' not in action + detail = client.get(f'/admin/api/goals/{first}').json() + assert detail['auto_review']['duplicate_judgments'][0]['undoable'] is True + path = f"/admin/api/goal-duplicate-judgments/{action['id']}/undo" + assert client.post(path, json={'expected_revision': action['target_revision']}, + headers={'X-Iris-CSRF': ''}).status_code == 403 + assert client.post(path, json={'expected_revision': action['target_revision'] - 1}).status_code == 409 + response = client.post(path, json={'expected_revision': action['target_revision']}) + assert response.status_code == 200, response.text + assert response.json()['id'] == first and row(store, second)['merged_into'] is None + action, = client.get('/admin/api/goal-auto-actions').json()['items'] + assert action['undoable'] is False and action['undone_at'] + assert client.post(path, json={'expected_revision': row(store, first)['revision']}).status_code == 400 diff --git a/tests/test_goal_duplicate_auto.py b/tests/test_goal_duplicate_auto.py new file mode 100644 index 00000000..2a2f28e4 --- /dev/null +++ b/tests/test_goal_duplicate_auto.py @@ -0,0 +1,734 @@ +"""G2: daily judgment of "possible duplicate" goal pairs with the shared D6 material.""" +import json + +import pytest + +from iris import goals as goals_module +from iris.goals import DUPLICATE_AUTO_BUDGET_SECONDS, Goals +from iris.models import ModelError +from goal_auto_helpers import Clock, enable, items, row, run_goals + + +class Judge: + """Fake goal_dedup_judge purpose; also serves G1's chat purpose as unused.""" + + def __init__(self, store, verdict='uncertain'): + self.store, self.verdict = store, verdict + self.calls = [] + + def goal_dedup_judge(self, messages, ids, *, budget_seconds): + assert not self.store._writer.in_transaction + self.calls.append({'payload': json.loads(messages[-1]['content']), 'budget_seconds': budget_seconds, + 'system': messages[0]['content']}) + if isinstance(self.verdict, Exception): + raise self.verdict + return {'decisions': {gid: self.verdict for gid in ids}} + + +@pytest.fixture +def pair(store): + store.set_setting('goal_dedup_judge', {'enabled': True, 'budget_seconds': 1}) + judge = Judge(store) + goals = Goals(store, gateway=judge, clock=Clock('2026-11-02T09:00:00+00:00')) + + def make(first_origin='host', second_origin='host'): + first = goals.create(content='周五前整理操作手册', origin=first_origin, actor=first_origin) + goals.clock = Clock('2026-11-02T10:00:00+00:00') + second = goals.create(content='周五前整理操作说明手册', origin=second_origin, actor=second_origin) + assert second['dedup']['status'] == 'possible_duplicate' + judge.calls.clear() + return first['goal']['id'], second['submitted_id'] + return make, judge + + +def status(store, a, b): + with store.read() as conn: + return conn.execute('SELECT status,resolved_by FROM goal_duplicates WHERE goal_a=? AND goal_b=?', + (min(a, b), max(a, b))).fetchone() + + +def judgments(store): + with store.read() as conn: + return [dict(r) for r in conn.execute('SELECT * FROM goal_duplicate_judgments ORDER BY id')] + + +def test_same_merges_into_the_earlier_goal_with_60_second_budget(store, pair): + make, judge = pair + first, second = make() + enable(store, review=False, duplicates=True) + judge.verdict = 'same' + engine, rid = run_goals(store, judge) + call, = judge.calls + assert DUPLICATE_AUTO_BUDGET_SECONDS == 60 and 59 < call['budget_seconds'] <= 60 # not the 5 s inline budget + assert call['payload']['incoming']['id'] == second + assert [c['id'] for c in call['payload']['candidates']] == [first] + assert row(store, second)['merged_into'] == first and row(store, first)['state'] == 'open' + assert tuple(status(store, first, second)) == ('merged', 'maintenance') + record, = judgments(store) + assert (record['outcome'], record['verdict'], record['reason'], record['target_id']) == ('merged', 'same', 'auto_merged', first) + with store.read() as conn: + merge = conn.execute("SELECT * FROM goal_revisions WHERE action='merge' AND goal_id=?", (first,)).fetchone() + assert (merge['actor'], merge['reason']) == ('maintenance', 'auto_merged') + item, = items(store, rid) + assert (item['outcome'], item['object_id']) == ('goal_duplicates_merged', first) + assert engine.report(rid)['goal_duplicate_review']['merged'] == 1 + + +def test_different_withdraws_the_hint(store, pair): + make, judge = pair + first, second = make() + enable(store, review=False, duplicates=True) + judge.verdict = 'different' + run_goals(store, judge) + assert tuple(status(store, first, second)) == ('dismissed', 'maintenance') + assert row(store, first)['merged_into'] is None and row(store, second)['merged_into'] is None + assert judgments(store)[0]['reason'] == 'auto_different' + with store.read() as conn: + snapshot = conn.execute("SELECT * FROM goal_revisions WHERE action='duplicate_dismiss' AND goal_id=?", (first,)).fetchone() + assert (snapshot['actor'], snapshot['reason']) == ('maintenance', 'auto_different') + + +def test_uncertain_keeps_the_hint_and_is_not_judged_again(store, pair): + make, judge = pair + first, second = make() + enable(store, review=False, duplicates=True) + run_goals(store, judge) + assert tuple(status(store, first, second))[0] == 'possible' + assert judgments(store)[0]['outcome'] == 'kept' and judgments(store)[0]['reason'] == 'uncertain' + engine, rid = run_goals(store, judge) + assert len(judge.calls) == 1 + assert engine.report(rid)['goal_duplicate_review']['already_judged'] == 1 + + +@pytest.mark.parametrize('origins', [('admin', 'host'), ('host', 'admin')]) +def test_admin_goal_is_never_merged_and_the_hint_is_withdrawn(store, pair, origins): + make, judge = pair + first, second = make(*origins) + enable(store, review=False, duplicates=True) + judge.verdict = 'same' + run_goals(store, judge) + assert row(store, first)['merged_into'] is None and row(store, second)['merged_into'] is None + assert tuple(status(store, first, second)) == ('dismissed', 'maintenance') + assert judgments(store)[0]['reason'] == 'admin_not_merged' + + +def test_timeout_keeps_the_hint_and_retries_next_time(store, pair): + make, judge = pair + first, second = make() + enable(store, review=False, duplicates=True) + judge.verdict = ModelError('retryable', 'goal judgment total timeout', reason='timeout') + engine, rid = run_goals(store, judge) + assert tuple(status(store, first, second))[0] == 'possible' and judgments(store) == [] + item, = items(store, rid) + assert (item['outcome'], item['reason']) == ('failed', 'timeout') + judge.verdict = 'same' + run_goals(store, judge) + assert len(judge.calls) == 2 and row(store, second)['merged_into'] == first + + +def test_switch_off_means_no_dedup_call(store, pair): + make, judge = pair + make() + run_goals(store, judge) + enable(store, review=True, duplicates=False) + run_goals(store, judge) + assert judge.calls == [] and judgments(store) == [] + + +def test_disabled_dedup_judge_stops_g2(store, pair): + make, judge = pair + make() + store.set_setting('goal_dedup_judge', {'enabled': False}) + enable(store, review=False, duplicates=True) + engine, rid = run_goals(store, judge) + assert judge.calls == [] and engine.report(rid)['goal_duplicate_review']['stopped'] == 'dedup_judge_disabled' + + +def test_changed_pair_during_call_is_skipped_without_fingerprint(store, pair): + make, judge = pair + first, second = make() + enable(store, review=False, duplicates=True) + judge.verdict = 'same' + original = judge.goal_dedup_judge + + def edit(messages, ids, *, budget_seconds): + result = original(messages, ids, budget_seconds=budget_seconds) + current = row(store, first) + Goals(store, clock=Clock()).update(first, expected_revision=current['revision'], deadline='2026-11-06', actor='admin') + return result + judge.goal_dedup_judge = edit + engine, rid = run_goals(store, judge) + assert judgments(store) == [] and row(store, second)['merged_into'] is None + assert engine.report(rid)['goal_duplicate_review']['revision_conflict'] == 1 + + +def test_creation_review_and_g2_share_one_material_assembly(store, pair, monkeypatch): + make, judge = pair + calls = [] + original = goals_module.dedup_review_material + + def spy(conn, goal_id, options, **kwargs): + calls.append(goal_id) + return original(conn, goal_id, options, **kwargs) + monkeypatch.setattr(goals_module, 'dedup_review_material', spy) + first, second = make() + assert second in calls # the creation-time review (what goal_dedup_probe drives) + calls.clear() + enable(store, review=False, duplicates=True) + run_goals(store, judge) + assert calls == [second] + with store.read() as conn: + review = original(conn, second, goals_module.dedup_judge.connection_settings(conn)) + assert judge.calls[0]['payload'] == {'incoming': review['incoming'], 'candidates': review['materials']} + assert judge.calls[0]['system'] == goals_module.dedup_judge.judgment_prompt('D6') + + +class RuleJudge(Judge): + """Verdict per (incoming, candidate); unknown pairs are uncertain.""" + + def __init__(self, store): + super().__init__(store) + self.verdicts = {} + + def goal_dedup_judge(self, messages, ids, *, budget_seconds): + payload = json.loads(messages[-1]['content']) + self.calls.append({'incoming': payload['incoming']['id'], 'candidates': [c['id'] for c in payload['candidates']]}) + return {'decisions': {gid: self.verdicts.get((payload['incoming']['id'], gid), 'uncertain') for gid in ids}} + + +def _scoped_flow(store, host_entries): + """Natural flow: I2 merges into I, re-pointing (I2,H) to (I,H) across the host's scope.""" + from iris.goals import write_learning_goal + from iris.tokens import Scope + from goal_auto_helpers import entry, goal + store.set_setting('goal_dedup_judge', {'enabled': True, 'budget_seconds': 1}) + judge = RuleJudge(store) + with store.write() as conn: + for name in ('e1', 'e2'): + entry(conn, name) + inner = goal(store, '周五前整理操作手册', people=(), entry_id='e2', created='2026-11-02T09:00:00+00:00') + scope = Scope(kind='entries', entries=host_entries) + goals = Goals(store, gateway=judge, clock=Clock('2026-11-02T10:00:00+00:00')) + host = goals.create(content='周五前整理操作说明手册', origin='host', actor='h', host='h', entry_id='e1', + scope=scope)['submitted_id'] + goals.clock = Clock('2026-11-02T11:00:00+00:00') + with store.write() as conn: + third = write_learning_goal(conn, content='周五前把操作手册整理好', kind='normal', deadline=None, entry_id='e1', + evidence=[], current=goals.clock())['submitted_id'] + goals.review(third) + enable(store, review=False, duplicates=True) + judge.verdicts.update({(third, inner): 'same', (third, host): 'different'}) + run_goals(store, judge) + assert row(store, third)['merged_into'] == inner + assert tuple(status(store, inner, host))[0] == 'possible' # re-pointed by the merge + judge.verdicts[(host, inner)] = 'same' + return inner, host, third, scope, judge + + +def test_host_goal_is_never_merged_outside_its_token_scope(store): + inner, host, third, scope, judge = _scoped_flow(store, ('e1',)) + calls = len(judge.calls) + engine, rid = run_goals(store, judge) + assert len(judge.calls) == calls # no model call for a pair that can never merge + assert row(store, host)['merged_into'] is None and row(store, inner)['merged_into'] is None + assert tuple(status(store, inner, host))[0] == 'possible' # the hint stays for the administrator + record = judgments(store)[-1] + assert (record['outcome'], record['reason'], record['verdict']) == ('kept', 'host_scope_not_merged', None) + assert engine.report(rid)['goal_duplicate_review']['kept'] == 1 + engine, rid = run_goals(store, judge) + assert len(judge.calls) == calls and engine.report(rid)['goal_duplicate_review']['already_judged'] == 1 + # The host keeps writing its own goal within its token scope. + updated = Goals(store, clock=Clock()).update(host, content='周五前整理操作说明', actor='h', scope=scope) + assert updated['content'] == '周五前整理操作说明' + + +def test_host_goal_merges_when_the_partner_is_inside_its_scope(store): + inner, host, third, scope, judge = _scoped_flow(store, ('e1', 'e2')) + run_goals(store, judge) + assert row(store, host)['merged_into'] == inner + assert judgments(store)[-1]['reason'] == 'auto_merged' + + +def _merged(store, pair, *, deadline=None): + make, judge = pair + first, second = make() + if deadline: + current = row(store, second) + Goals(store, clock=Clock('2026-11-02T10:30:00+00:00')).update( + second, expected_revision=current['revision'], deadline=deadline, actor='admin') + enable(store, review=False, duplicates=True) + judge.verdict = 'same' + engine, rid = run_goals(store, judge) + assert row(store, second)['merged_into'] == first + return first, second, judgments(store)[-1], judge, engine, rid + + +def test_undo_restores_both_goals_and_never_merges_the_pair_again(store, pair): + first, second, judgment, judge, engine, rid = _merged(store, pair, deadline='2026-11-20') + merged_target = row(store, first) + assert merged_target['deadline'] is not None # filled from the merged goal + item, = [i for i in items(store, rid) if i['outcome'] == 'goal_duplicates_merged'] + assert item['details']['target_revision_after'] == merged_target['revision'] + goals = Goals(store, clock=Clock()) + restored = goals.undo_auto_merge(judgment['id'], expected_revision=merged_target['revision']) + assert restored['id'] == first and restored['deadline'] is None + assert row(store, second)['merged_into'] is None and row(store, second)['deadline'] is not None + assert tuple(status(store, first, second)) == ('dismissed', 'admin') + with store.read() as conn: + record = dict(conn.execute('SELECT * FROM goal_duplicate_judgments WHERE id=?', (judgment['id'],)).fetchone()) + actions = [r[0] for r in conn.execute("SELECT action FROM goal_revisions WHERE action='merge_undo'")] + operation = conn.execute("SELECT * FROM admin_operations WHERE action='goal_merge_undo'").fetchone() + plans = conn.execute("SELECT COUNT(*) FROM goal_reminder_plans WHERE goal_id=? AND status='scheduled'", (second,)).fetchone()[0] + assert record['undone_by'] == 'admin' and record['undone_at'] and len(actions) >= 2 and operation['actor'] == 'admin' + assert plans >= 1 # the restored goal's own deadline is scheduled again + calls = len(judge.calls) + run_goals(store, judge) + assert len(judge.calls) == calls and row(store, second)['merged_into'] is None + with pytest.raises(goals_module.GoalError): + goals.undo_auto_merge(judgment['id'], expected_revision=row(store, first)['revision']) + + +def test_undo_refuses_when_the_survivor_changed_after_the_merge(store, pair): + first, second, judgment, judge, engine, rid = _merged(store, pair) + goals = Goals(store, clock=Clock()) + current = row(store, first) + with pytest.raises(goals_module.GoalConflict): + goals.undo_auto_merge(judgment['id'], expected_revision=current['revision'] - 1) + edited = goals.update(first, expected_revision=current['revision'], content='周五前整理好操作手册', actor='admin') + with pytest.raises(goals_module.GoalConflict): + goals.undo_auto_merge(judgment['id'], expected_revision=edited['revision']) + assert row(store, second)['merged_into'] == first + + +def test_only_automatic_merges_can_be_undone(store, pair): + make, judge = pair + first, second = make() + goals = Goals(store, clock=Clock()) + a, b = row(store, first), row(store, second) + goals.merge(first, second, expected_revision=a['revision'], other_revision=b['revision']) + with store.write() as conn: + jid = conn.execute("""INSERT INTO goal_duplicate_judgments(goal_a,goal_b,fingerprint,method,verdict,outcome, + reason,target_id,created_at) VALUES(?,?,'x','D6','same','merged','auto_merged',?,?)""", + (first, second, first, '2026-11-03T00:00:00+00:00')).lastrowid + with pytest.raises(goals_module.GoalError): + goals.undo_auto_merge(jid, expected_revision=row(store, first)['revision']) + with pytest.raises(KeyError): + goals.undo_auto_merge(jid + 100, expected_revision=1) + + +def test_undo_returns_moved_reminders_to_the_restored_goal(store, pair): + make, judge = pair + first, second = make() + goals = Goals(store, clock=Clock('2026-11-02T10:30:00+00:00')) + current = row(store, second) + goals.update(second, expected_revision=current['revision'], deadline='2026-11-02T11:00:00+00:00', actor='admin') + with store.read() as conn: + pending = [r[0] for r in conn.execute("SELECT id FROM notifications WHERE goal_id=? AND status='pending'", (second,))] + assert pending # immediate "due soon" reminder for the second goal + enable(store, review=False, duplicates=True) + judge.verdict = 'same' + run_goals(store, judge) + with store.read() as conn: + assert conn.execute('SELECT goal_id FROM notifications WHERE id=?', (pending[0],)).fetchone()[0] == first + judgment = judgments(store)[-1] + Goals(store, clock=Clock()).undo_auto_merge(judgment['id'], expected_revision=row(store, first)['revision']) + with store.read() as conn: + note = conn.execute('SELECT * FROM notifications WHERE id=?', (pending[0],)).fetchone() + fresh = conn.execute("SELECT * FROM notifications WHERE goal_id=? AND status='pending'", (second,)).fetchall() + assert (note['goal_id'], note['status']) == (first, 'cancelled') # the record itself is not rewritten + assert len(fresh) == 1 and fresh[0]['id'] > pending[0] # a new ID a host cursor can still reach + assert fresh[0]['content'].endswith(row(store, second)['content']) + + +def test_undo_removes_relations_the_merge_copied_and_restores_watermarks(store): + from goal_auto_helpers import CREATED, goal, message, subject + subject(store, 'p1', '程栗') + subject(store, 'p2', '路人') + s1 = message(store, '周五前整理操作手册', at='2026-11-02T16:00:00+08:00') + s2 = message(store, '周五前把操作说明手册整理好', sender='p2', at='2026-11-02T17:30:00+08:00') + first = goal(store, '周五前整理操作手册', people=(), sources=[s1], created='2026-11-02T09:00:00+00:00') + second = goal(store, '周五前整理操作说明手册', people=(), sources=[s2], created='2026-11-02T10:00:00+00:00') + with store.write() as conn: + conn.execute('UPDATE goals SET auto_review_after_message_id=? WHERE id=?', (s2, second)) + conn.execute("INSERT INTO goal_duplicates(goal_a,goal_b,status,created_at) VALUES(?,?,'possible',?)", + (first, second, CREATED)) + store.set_setting('goal_dedup_judge', {'enabled': True, 'budget_seconds': 1}) + judge = Judge(store, 'same') + enable(store, review=False, duplicates=True) + run_goals(store, judge) + with store.read() as conn: + merged_sources = sorted(r[0] for r in conn.execute('SELECT message_id FROM goal_sources WHERE goal_id=?', (first,))) + assert merged_sources == [s1, s2] and row(store, first)['auto_review_after_message_id'] == s2 + Goals(store, clock=Clock()).undo_auto_merge(judgments(store)[-1]['id'], expected_revision=row(store, first)['revision']) + with store.read() as conn: + assert [r[0] for r in conn.execute('SELECT message_id FROM goal_sources WHERE goal_id=?', (first,))] == [s1] + assert [r[0] for r in conn.execute('SELECT message_id FROM goal_sources WHERE goal_id=?', (second,))] == [s2] + assert row(store, first)['auto_review_after_message_id'] is None + assert row(store, second)['auto_review_after_message_id'] == s2 and row(store, second)['merged_into'] is None + + + +def test_internal_goal_merges_into_an_earlier_scoped_host_goal_as_at_creation(store): + from iris.goals import write_learning_goal + from iris.tokens import Scope + from goal_auto_helpers import entry + store.set_setting('goal_dedup_judge', {'enabled': True, 'budget_seconds': 1}) + judge = Judge(store, 'uncertain') + with store.write() as conn: + for name in ('e1', 'e2'): + entry(conn, name) + goals = Goals(store, gateway=judge, clock=Clock('2026-11-02T09:00:00+00:00')) + host = goals.create(content='周五前整理操作说明手册', origin='host', actor='h', host='h', entry_id='e1', + scope=Scope(kind='entries', entries=('e1',)))['submitted_id'] + goals.clock = Clock('2026-11-02T10:00:00+00:00') + with store.write() as conn: + later = write_learning_goal(conn, content='周五前整理操作手册', kind='normal', deadline=None, entry_id='e2', + evidence=[], current=goals.clock())['submitted_id'] + assert goals.review(later)['dedup']['status'] == 'possible_duplicate' + judge.verdict, judge.calls = 'same', [] + enable(store, review=False, duplicates=True) + run_goals(store, judge) + assert [c['payload']['incoming']['id'] for c in judge.calls] == [later] + assert row(store, later)['merged_into'] == host and row(store, host)['entry_id'] == 'e1' + assert judgments(store)[-1]['reason'] == 'auto_merged' + + +def test_undo_reissues_the_current_stage_for_both_goals_of_each_merge(store): + from iris import goals as gm + from goal_auto_helpers import goal, subject + store.set_setting('goal_dedup_judge', {'enabled': True, 'budget_seconds': 1}) + subject(store, 'p1', '程栗') + own = goal(store, '周五前整理操作手册', people=('p1',), created='2026-11-02T08:10:00+00:00', + deadline='2026-11-03T03:00:00+00:00') + other = goal(store, '周五前整理操作说明手册', people=(), created='2026-11-02T09:40:00+00:00') + bare = goal(store, '周五前整理产品手册', people=(), created='2026-11-02T08:00:00+00:00') + later = goal(store, '周五前整理产品说明手册', people=(), created='2026-11-02T09:50:00+00:00', + deadline='2026-11-03T03:00:00+00:00') + with store.write() as conn: + for a, b in ((own, other), (bare, later)): + conn.execute("INSERT INTO goal_duplicates(goal_a,goal_b,status,created_at) VALUES(?,?,'possible','x')", (a, b)) + for gid in (own, later): + gm._schedule(conn, gm._row(conn, gid), Clock('2026-11-02T10:00:00+00:00')()) + judge = RuleJudge(store) + judge.verdicts.update({(other, own): 'same', (other, bare): 'different', (other, later): 'different', + (later, bare): 'same', (later, own): 'different', (later, other): 'different'}) + enable(store, review=False, duplicates=True) + run_goals(store, judge, Clock('2026-11-03T01:00:00+00:00')) + assert row(store, other)['merged_into'] == own and row(store, later)['merged_into'] == bare + Goals(store, clock=Clock('2026-11-03T02:30:00+00:00')).generate_notifications() + before = _all_notes(store) + undo = Goals(store, clock=Clock('2026-11-03T02:40:00+00:00')) + for jid, survivor in ((j['id'], j['target_id']) for j in judgments(store) if j['outcome'] == 'merged'): + undo.undo_auto_merge(jid, expected_revision=row(store, survivor)['revision']) + _assert_undo_reminders(store, before, due_soon=[own, later], silent=[other, bare]) + + + +def test_absorbed_host_goal_blocks_a_merge_out_of_its_scope(store): + """probe_scope_redirect: H merged into S; S must not then be merged into P outside H's scope.""" + _absorbed_scope_flow(store, host_entries=('e1',), blocked=True) + + +def test_absorbed_host_goal_allows_a_merge_inside_its_scope(store): + _absorbed_scope_flow(store, host_entries=('e1', 'e2'), blocked=False) + + +def _absorbed_scope_flow(store, *, host_entries, blocked): + from iris.goals import write_learning_goal + from iris.tokens import Scope + from goal_auto_helpers import entry, goal + store.set_setting('goal_dedup_judge', {'enabled': True, 'budget_seconds': 1}) + judge = RuleJudge(store) + with store.write() as conn: + for name in ('e1', 'e2'): + entry(conn, name) + inner = goal(store, '周五前整理操作手册', people=(), entry_id='e2', created='2026-11-02T08:00:00+00:00') + goals = Goals(store, gateway=judge, clock=Clock('2026-11-02T09:00:00+00:00')) + with store.write() as conn: + survivor = write_learning_goal(conn, content='周五前把操作手册整理好', kind='normal', deadline=None, entry_id='e1', + evidence=[], current=goals.clock())['submitted_id'] + goals.review(survivor) + scope = Scope(kind='entries', entries=host_entries) + goals.clock = Clock('2026-11-02T10:00:00+00:00') + host = goals.create(content='周五前整理操作说明手册', origin='host', actor='h', host='h', entry_id='e1', + scope=scope)['submitted_id'] + assert tuple(status(store, inner, survivor))[0] == 'possible' + assert tuple(status(store, survivor, host))[0] == 'possible' + enable(store, review=False, duplicates=True) + judge.verdicts[(host, survivor)] = 'same' + judge.verdicts[(host, inner)] = 'different' + run_goals(store, judge, Clock('2026-11-03T01:00:00+00:00')) + assert row(store, host)['merged_into'] == survivor and row(store, survivor)['merged_into'] is None + judge.verdicts[(survivor, inner)] = 'same' + calls = len(judge.calls) + run_goals(store, judge, Clock('2026-11-04T01:00:00+00:00')) + if blocked: + assert len(judge.calls) == calls # no model call + assert row(store, survivor)['merged_into'] is None and row(store, host)['merged_into'] == survivor + assert tuple(status(store, inner, survivor))[0] == 'possible' + record = judgments(store)[-1] + assert (record['outcome'], record['reason']) == ('kept', 'host_scope_not_merged') + # The host still reaches its goal through the redirect, inside its scope. + with pytest.raises(goals_module.GoalMerged) as merged: + Goals(store, clock=Clock()).update(host, content='x', actor='h', scope=scope) + assert merged.value.canonical_id == survivor + updated = Goals(store, clock=Clock()).update(survivor, content='周五前整理好操作手册', actor='h', scope=scope) + assert updated['id'] == survivor + else: + assert row(store, survivor)['merged_into'] == inner and row(store, host)['merged_into'] == inner + + +def test_undo_round3_reminder_fired_under_a_for_bs_deadline(store): + """probe_round3: A has no deadline, B has one; B's warning fired under A before the undo.""" + from iris import goals as gm + from goal_auto_helpers import goal, subject + store.set_setting('goal_dedup_judge', {'enabled': True, 'budget_seconds': 1}) + subject(store, 'p1', '程栗') + a = goal(store, '周五前整理操作手册', people=('p1',), created='2026-11-02T08:10:00+00:00') + b = goal(store, '周五前整理操作说明手册', people=(), created='2026-11-02T09:40:00+00:00', + deadline='2026-11-03T03:00:00+00:00') + with store.write() as conn: + conn.execute("INSERT INTO goal_duplicates(goal_a,goal_b,status,created_at) VALUES(?,?,'possible','x')", (a, b)) + gm._schedule(conn, gm._row(conn, b), Clock('2026-11-02T10:00:00+00:00')()) + enable(store, review=False, duplicates=True) + run_goals(store, Judge(store, 'same'), Clock('2026-11-03T01:00:00+00:00')) + assert row(store, b)['merged_into'] == a + Goals(store, clock=Clock('2026-11-03T02:30:00+00:00')).generate_notifications() + before = _all_notes(store) + jid = judgments(store)[-1]['id'] + Goals(store, clock=Clock('2026-11-03T02:40:00+00:00')).undo_auto_merge(jid, expected_revision=row(store, a)['revision']) + _assert_undo_reminders(store, before, due_soon=[b], silent=[a]) + with store.read() as conn: + plans = [r[0] for r in conn.execute("SELECT reminder_kind FROM goal_reminder_plans WHERE goal_id=? AND status='scheduled'", (b,))] + assert plans == ['due'] # B's own plan continues + + + +def test_undo_with_equal_deadlines_reissues_for_both(store): + from iris import goals as gm + from goal_auto_helpers import goal, subject + store.set_setting('goal_dedup_judge', {'enabled': True, 'budget_seconds': 1}) + subject(store, 'p1', '程栗') + deadline = '2026-11-03T03:00:00+00:00' + a = goal(store, '周五前整理操作手册', people=('p1',), created='2026-11-02T08:10:00+00:00', deadline=deadline) + b = goal(store, '周五前整理操作说明手册', people=(), created='2026-11-02T09:40:00+00:00', deadline=deadline) + with store.write() as conn: + conn.execute("INSERT INTO goal_duplicates(goal_a,goal_b,status,created_at) VALUES(?,?,'possible','x')", (a, b)) + for gid in (a, b): + gm._schedule(conn, gm._row(conn, gid), Clock('2026-11-02T10:00:00+00:00')()) + enable(store, review=False, duplicates=True) + run_goals(store, Judge(store, 'same'), Clock('2026-11-03T01:00:00+00:00')) + Goals(store, clock=Clock('2026-11-03T02:30:00+00:00')).generate_notifications() + before = _all_notes(store) + jid = judgments(store)[-1]['id'] + Goals(store, clock=Clock('2026-11-03T02:40:00+00:00')).undo_auto_merge(jid, expected_revision=row(store, a)['revision']) + _assert_undo_reminders(store, before, due_soon=[a, b]) + + + +def _long_lead_merge(store, *, deadline, take): + """probe_undo_dup: the survivor's longer lead fires B's deadline early under the survivor.""" + from iris import goals as gm + from goal_auto_helpers import goal, subject + store.set_setting('goal_dedup_judge', {'enabled': True, 'budget_seconds': 1}) + subject(store, 'p1', '程栗') + a = goal(store, '周五前整理操作手册', people=('p1',), created='2026-11-02T08:10:00+00:00') + b = goal(store, '周五前整理操作说明手册', people=(), created='2026-11-02T09:40:00+00:00', deadline=deadline) + with store.write() as conn: + conn.execute('UPDATE goals SET reminder_minutes=600 WHERE id=?', (a,)) + conn.execute("INSERT INTO goal_duplicates(goal_a,goal_b,status,created_at) VALUES(?,?,'possible','x')", (a, b)) + gm._schedule(conn, gm._row(conn, b), Clock('2026-11-02T10:00:00+00:00')()) + enable(store, review=False, duplicates=True) + run_goals(store, Judge(store, 'same'), Clock('2026-11-03T01:00:00+00:00')) + Goals(store, clock=Clock('2026-11-03T02:30:00+00:00')).generate_notifications() + with store.write() as conn: + early, = [r[0] for r in conn.execute("SELECT id FROM notifications WHERE goal_id=? AND status='pending'", (a,))] + if take: + conn.execute("UPDATE notifications SET status='taken',taken_at=? WHERE id=?", ('2026-11-03T02:35:00+00:00', early)) + before = dict(conn.execute('SELECT * FROM notifications WHERE id=?', (early,)).fetchone()) + Goals(store, clock=Clock('2026-11-03T03:00:00+00:00')).undo_auto_merge( + judgments(store)[-1]['id'], expected_revision=row(store, a)['revision']) + return a, b, early, before + + +def _notes(store, gid): + with store.read() as conn: + return [dict(r) for r in conn.execute('SELECT * FROM notifications WHERE goal_id=? ORDER BY id', (gid,))] + + +@pytest.mark.parametrize('take', [False, True]) +def test_undo_never_rewrites_existing_reminders_and_b_gets_exactly_one_on_time(store, take): + a, b, early, before = _long_lead_merge(store, deadline='2026-11-03T12:00:00+00:00', take=take) + with store.read() as conn: + after = dict(conn.execute('SELECT * FROM notifications WHERE id=?', (early,)).fetchone()) + assert (after['goal_id'], after['content']) == (before['goal_id'], before['content']) == (a, before['content']) + assert after['status'] == ('taken' if take else 'cancelled') + assert _notes(store, b) == [] # B's own warning (11:00) is still ahead: no new record yet + Goals(store, clock=Clock('2026-11-03T11:00:00+00:00')).generate_notifications() # B's own warning time + pending = [n for n in _notes(store, b) if n['status'] == 'pending'] + assert len(pending) == 1 and pending[0]['reminder_kind'] == 'soon' and pending[0]['id'] > early + + +def test_undo_reissues_immediately_when_bs_own_warning_time_has_passed(store): + a, b, early, before = _long_lead_merge(store, deadline='2026-11-03T03:30:00+00:00', take=False) + notes = _notes(store, b) + assert len(notes) == 1 and notes[0]['status'] == 'pending' and notes[0]['id'] > early + assert notes[0]['reminder_kind'] == 'immediate' and notes[0]['content'].startswith('目标临近截止') + + + +def _all_notes(store): + with store.read() as conn: + return {r['id']: dict(r) for r in conn.execute('SELECT * FROM notifications ORDER BY id')} + + +def _assert_undo_reminders(store, before, *, due_soon=(), overdue=(), due=(), silent=()): + """Undo never moves, rewrites or revives a record; each goal gets at most one fresh one.""" + after = _all_notes(store) + involved = {*due_soon, *overdue, *due, *silent} + for nid, old in before.items(): + new = after[nid] + assert (new['goal_id'], new['content']) == (old['goal_id'], old['content']) + if old['goal_id'] in involved: # pending ones are cancelled, nothing is revived, taken ones stay + assert new['status'] == ('cancelled' if old['status'] == 'pending' else old['status']) + fresh = [n for nid, n in after.items() if nid not in before] + assert len(fresh) <= len(due_soon) + len(overdue) + len(due) + for gid in (*due_soon, *overdue, *due): + mine = [n for n in fresh if n['goal_id'] == gid] + assert len(mine) == 1 and mine[0]['status'] == 'pending' + assert mine[0]['reminder_kind'] == ('due' if gid in due else 'immediate') + assert mine[0]['content'].startswith('已过期' if gid in overdue else '目标已到期' if gid in due else '目标临近截止') + assert mine[0]['content'].endswith(row(store, gid)['content']) + for gid in silent: + assert [n for n in after.values() if n['goal_id'] == gid and n['status'] == 'pending'] == [] + + +def _round5_setup(store, lead_a, lead_b): + from iris import goals as gm + from goal_auto_helpers import goal, subject + store.set_setting('goal_dedup_judge', {'enabled': True, 'budget_seconds': 1}) + subject(store, 'p1', '程栗') + a = goal(store, '周五前整理操作手册', people=('p1',), created='2026-11-02T08:10:00+00:00', + deadline='2026-11-03T12:00:00+00:00') + b = goal(store, '周五前整理操作说明手册', people=(), created='2026-11-02T09:40:00+00:00', + deadline='2026-11-03T12:00:00+00:00') + with store.write() as conn: + conn.execute('UPDATE goals SET reminder_minutes=? WHERE id=?', (lead_a, a)) + conn.execute('UPDATE goals SET reminder_minutes=? WHERE id=?', (lead_b, b)) + conn.execute("INSERT INTO goal_duplicates(goal_a,goal_b,status,created_at) VALUES(?,?,'possible','x')", (a, b)) + for gid in (a, b): + gm._schedule(conn, gm._row(conn, gid), Clock('2026-11-02T10:00:00+00:00')()) + return a, b + + +def _generate(store, at): + goals = Goals(store, clock=Clock(at)) + goals._first_generation = False + goals.generate_notifications() + return goals + + +def test_undo_round5_after_the_kept_reminder_was_taken_both_goals_get_their_stage(store): + """probe_undo_round5 (1): B warned first, A later; the merge kept one under A; the host took it.""" + a, b = _round5_setup(store, 60, 120) + _generate(store, '2026-11-03T10:01:00+00:00') + _generate(store, '2026-11-03T11:01:00+00:00') + enable(store, review=False, duplicates=True) + run_goals(store, Judge(store, 'same'), Clock('2026-11-03T11:30:00+00:00')) + host = _generate(store, '2026-11-03T11:40:00+00:00') + first = host.pull(after=0) + assert first['items'] + before = _all_notes(store) + jid = judgments(store)[-1]['id'] + Goals(store, clock=Clock('2026-11-03T11:50:00+00:00')).undo_auto_merge(jid, expected_revision=row(store, a)['revision']) + _assert_undo_reminders(store, before, due_soon=[a, b]) + host.clock = Clock('2026-11-03T11:55:00+00:00') + again = host.pull(after=first['next_cursor']) + assert sorted(item['goal_id'] for item in again['items']) == sorted([a, b]) # neither goal loses its warning + + +def test_undo_round5_equal_deadlines_merged_before_the_warning(store): + """probe_undo_round5 (2): one warning fired for A while merged; undo while it is pending.""" + a, b = _round5_setup(store, 60, 60) + enable(store, review=False, duplicates=True) + run_goals(store, Judge(store, 'same'), Clock('2026-11-03T01:00:00+00:00')) + _generate(store, '2026-11-03T11:05:00+00:00') + before = _all_notes(store) + jid = judgments(store)[-1]['id'] + Goals(store, clock=Clock('2026-11-03T11:10:00+00:00')).undo_auto_merge(jid, expected_revision=row(store, a)['revision']) + _assert_undo_reminders(store, before, due_soon=[a, b]) + with store.read() as conn: + for gid in (a, b): + assert [r[0] for r in conn.execute("SELECT reminder_kind FROM goal_reminder_plans WHERE goal_id=? AND status='scheduled'", (gid,))] == ['due'] + + +def test_undo_past_the_deadline_issues_one_overdue_reminder_unless_turned_off(store): + from iris import goals as gm + from goal_auto_helpers import goal, subject + store.set_setting('goal_dedup_judge', {'enabled': True, 'budget_seconds': 1}) + subject(store, 'p1', '程栗') + a = goal(store, '周五前整理操作手册', people=('p1',), created='2026-11-02T08:10:00+00:00') + b = goal(store, '周五前整理操作说明手册', people=(), created='2026-11-02T09:40:00+00:00', + deadline='2026-11-03T03:00:00+00:00') + with store.write() as conn: + conn.execute("INSERT INTO goal_duplicates(goal_a,goal_b,status,created_at) VALUES(?,?,'possible','x')", (a, b)) + enable(store, review=False, duplicates=True) + run_goals(store, Judge(store, 'same'), Clock('2026-11-03T01:00:00+00:00')) + before = _all_notes(store) + jid = judgments(store)[-1]['id'] + Goals(store, clock=Clock('2026-11-03T05:00:00+00:00')).undo_auto_merge(jid, expected_revision=row(store, a)['revision']) + _assert_undo_reminders(store, before, overdue=[b], silent=[a]) + + +def test_undo_past_the_deadline_respects_turned_off_overdue_reminders(store): + from goal_auto_helpers import goal, subject + store.set_setting('goal_dedup_judge', {'enabled': True, 'budget_seconds': 1}) + store.set_setting('goals', {'overdue_reminders': False}) + subject(store, 'p1', '程栗') + a = goal(store, '周五前整理操作手册', people=('p1',), created='2026-11-02T08:10:00+00:00') + b = goal(store, '周五前整理操作说明手册', people=(), created='2026-11-02T09:40:00+00:00', + deadline='2026-11-03T03:00:00+00:00') + with store.write() as conn: + conn.execute("INSERT INTO goal_duplicates(goal_a,goal_b,status,created_at) VALUES(?,?,'possible','x')", (a, b)) + store.set_setting('goals', {'overdue_reminders': False, 'goal_duplicate_auto_judge_enabled': True}) + run_goals(store, Judge(store, 'same'), Clock('2026-11-03T01:00:00+00:00')) + before = _all_notes(store) + jid = judgments(store)[-1]['id'] + Goals(store, clock=Clock('2026-11-03T05:00:00+00:00')).undo_auto_merge(jid, expected_revision=row(store, a)['revision']) + _assert_undo_reminders(store, before, due=[b], silent=[a]) + with store.read() as conn: + assert [r[0] for r in conn.execute("SELECT reminder_kind FROM goal_reminder_plans WHERE goal_id=? AND status='scheduled'", (b,))] == [] + + + +@pytest.mark.parametrize('overdue', [False, True]) +def test_undo_round6_survivor_past_its_deadline_gets_its_stage_either_way(store, overdue): + """probe_undo_round6: A's own 'due' reminder is pending at undo, past A's deadline.""" + from iris import goals as gm + from iris.goals import AUTO_REVIEW_KEY, DUPLICATE_AUTO_KEY + from goal_auto_helpers import goal, subject + store.set_setting('goal_dedup_judge', {'enabled': True, 'budget_seconds': 1}) + store.set_setting('goals', {AUTO_REVIEW_KEY: False, DUPLICATE_AUTO_KEY: True, 'overdue_reminders': overdue}) + subject(store, 'p1', '程栗') + a = goal(store, '周五前整理操作手册', people=('p1',), created='2026-11-02T08:10:00+00:00', + deadline='2026-11-03T12:00:00+00:00') + b = goal(store, '周五前整理操作说明手册', people=(), created='2026-11-02T09:40:00+00:00') + with store.write() as conn: + conn.execute("INSERT INTO goal_duplicates(goal_a,goal_b,status,created_at) VALUES(?,?,'possible','x')", (a, b)) + gm._schedule(conn, gm._row(conn, a), Clock('2026-11-02T10:00:00+00:00')()) + run_goals(store, Judge(store, 'same'), Clock('2026-11-03T01:00:00+00:00')) + for at in ('2026-11-03T11:05:00+00:00', '2026-11-03T12:00:00+00:00'): + _generate(store, at) + with store.write() as conn: # the host took the soon reminder; the due one is still pending + conn.execute("UPDATE notifications SET status='taken' WHERE id=(SELECT MIN(id) FROM notifications)") + before = _all_notes(store) + assert [n['status'] for n in before.values()] == ['taken', 'pending'] + jid = judgments(store)[-1]['id'] + Goals(store, clock=Clock('2026-11-03T12:30:00+00:00')).undo_auto_merge(jid, expected_revision=row(store, a)['revision']) + if overdue: + _assert_undo_reminders(store, before, overdue=[a], silent=[b]) + else: + _assert_undo_reminders(store, before, due=[a], silent=[b]) + with store.read() as conn: + plans = [r[0] for r in conn.execute("SELECT reminder_kind FROM goal_reminder_plans WHERE goal_id=? AND status='scheduled'", (a,))] + assert plans == (['overdue'] if overdue else []) diff --git a/tests/test_goal_review_eval.py b/tests/test_goal_review_eval.py new file mode 100644 index 00000000..dd4746f1 --- /dev/null +++ b/tests/test_goal_review_eval.py @@ -0,0 +1,122 @@ +"""goal_review_eval: labels never reach the model; scoring is deterministic. Offline only.""" +import json +import tempfile +from importlib.util import module_from_spec, spec_from_file_location +from pathlib import Path + +import pytest + +ROOT = Path(__file__).resolve().parents[1] +spec = spec_from_file_location('goal_review_eval_run', ROOT / 'evals/goal_review_eval/run.py') +runner = module_from_spec(spec) +spec.loader.exec_module(runner) +PUBLIC = json.loads((ROOT / 'evals/goal_review_v1.json').read_text(encoding='utf-8')) + + +def subset(*ids): + return {**{k: v for k, v in PUBLIC.items() if k != 'cases'}, + 'cases': [case for case in PUBLIC['cases'] if case['id'] in ids]} + + +@pytest.fixture +def outside(): + # Evaluation output must be outside the repository (pytest's basetemp is inside). + with tempfile.TemporaryDirectory(prefix='g-c1-goal-review-eval-') as directory: + yield Path(directory) + + +def run(outside, corpus, mode, name='run'): + path = outside / 'corpus.json' + path.write_text(json.dumps(corpus, ensure_ascii=False), encoding='utf-8') + code = runner.main(['--corpus', str(path), '--out', str(outside / name), '--fake', mode]) + return code, json.loads((outside / name / 'report.json').read_text(encoding='utf-8')) + + +def test_product_input_drops_every_label(): + for case in PUBLIC['cases']: + material = runner.product_input(case) + assert set(material) == {'now', 'entries', 'subjects', 'goal', 'messages'} + assert not set(runner.LABEL_FIELDS) & set(material) + + +def test_labels_never_reach_the_model_and_scoring_is_deterministic(outside): + corpus = subset('GR001', 'GR005', 'GR009', 'GR012', 'GR017') + code, report = run(outside, corpus, 'completed') + assert code == 0 and report['valid'] and report['simulation'] is True and report['complete'] + requests = [] + for case in corpus['cases']: + requests += json.loads((outside / 'run/cases' / case['id'] / 'requests.json').read_text(encoding='utf-8')) + text = json.dumps(requests, ensure_ascii=False) + assert requests and all(r['purpose'] == 'goal_auto_review' for r in requests) + for case in corpus['cases']: + assert case['reason'] not in text and case['expected'].get('notes', '\0') not in text + for message in requests: + payload = json.loads(message['messages'][-1]['content']) + assert not {'expected', 'reason', 'category', 'split', 'id'} & set(payload) + assert 'expected' not in payload['goal'] and 'key' not in payload['goal'] + # The host goal is never sent to the model and never closed. + assert len(requests) == 4 + rows = {r['case_id']: r for r in report['results']} + assert rows['GR001']['correct'] and not rows['GR001']['false_close'] + assert rows['GR005']['false_close'] and rows['GR009']['false_close'] + assert rows['GR012']['actual'] == 'open' and report['external_closed']['count'] == 0 + assert rows['GR017']['expected'] == 'abandoned' and rows['GR017']['false_close'] # direction reversed + assert report['false_closures']['count'] == 3 + assert report['recognition'] == {'numerator': 1, 'denominator': 2, 'rate': .5} + assert report['categories']['self_done'] == {'cases': 1, 'correct': 1, 'false_closures': 0, 'actual': {'completed': 1}} + assert report['gates'] == {'false_closures_zero': False, 'external_closed_zero': True, + 'recognition_at_least_80_percent': False} + assert report['call_counts'] == {'calls': 4, 'timeouts': 0, 'rate_limited': 0, 'truncated': 0, + 'invalid_calls': 0, 'execution_errors': 0} + assert set(report) >= {'corpus_sha256', 'prompt_sha256', 'prompt_version', 'source_sha256', 'valid'} + + +def test_open_answers_close_nothing(outside): + code, report = run(outside, subset('GR001', 'GR005', 'GR012'), 'open') + assert code == 0 and report['valid'] + assert [r['actual'] for r in report['results']] == ['open', 'open', 'open'] + assert report['false_closures']['count'] == 0 and report['recognition']['rate'] == 0 + + +def test_invalid_output_is_a_quality_result_not_an_execution_failure(outside): + code, report = run(outside, subset('GR001'), 'invalid') + assert report['valid'] and report['results'][0]['actual'] == 'open' + assert report['results'][0]['review_reason'] == 'invalid_output' + assert report['call_counts']['calls'] == 2 # the answer plus one JSON repair + + +def test_timeout_stops_and_invalidates_the_run(outside): + code, report = run(outside, subset('GR001', 'GR002'), 'timeout') + assert code == 2 and not report['valid'] and not report['complete'] + assert report['cases'] == 1 and report['call_counts']['timeouts'] >= 1 + assert report['review_failures'][0]['case_id'] == 'GR001' + + +def test_output_inside_repository_and_bad_corpus_are_rejected(outside): + with pytest.raises(SystemExit): + runner.main(['--out', str(ROOT / 'evals' / 'goal-review-output'), '--fake', 'open']) + broken = subset('GR001') + broken['cases'][0]['expected'] = {'state': 'done'} + path = outside / 'broken.json' + path.write_text(json.dumps(broken, ensure_ascii=False), encoding='utf-8') + with pytest.raises(SystemExit): + runner.main(['--corpus', str(path), '--out', str(outside / 'x'), '--fake', 'open']) + assert not (outside / 'x').exists() and not (ROOT / 'evals' / 'goal-review-output').exists() + + +@pytest.mark.parametrize('expected,actual,false_close,correct', [ + ('open', 'open', False, True), ('open', 'completed', True, False), ('open', 'abandoned', True, False), + ('completed', 'completed', False, True), ('completed', 'abandoned', True, False), + ('completed', 'open', False, False), ('abandoned', 'completed', True, False), +]) +def test_score_case_directions(expected, actual, false_close, correct): + case = {'id': 'u', 'category': 'unit', 'goal': {'origin': 'internal'}, 'expected': {'state': expected}} + row = runner.score_case(case, {'state': actual}) + assert (row['false_close'], row['correct']) == (false_close, correct) + + +def test_external_closure_counts_separately(): + case = {'id': 'u', 'category': 'host_admin', 'goal': {'origin': 'host'}, 'expected': {'state': 'open'}} + row = runner.score_case(case, {'state': 'completed'}) + result = runner.report([row], [], expected_cases=1) + assert result['external_closed'] == {'count': 1, 'cases': ['u']} and not result['gates']['external_closed_zero'] diff --git a/tests/test_goals_api.py b/tests/test_goals_api.py index 0ff6ddf0..09275bb7 100644 --- a/tests/test_goals_api.py +++ b/tests/test_goals_api.py @@ -104,7 +104,8 @@ def test_admin_goal_crud_filters_settings_notifications_and_audit(client, store) assert client.get('/admin/api/goals').status_code == 409 login_admin(client) default = client.get('/admin/api/settings').json()['goals'] - assert default == {'default_reminder_minutes': 60, 'overdue_reminders': True, 'internal_question_expiry_days': 7} + assert default == {'default_reminder_minutes': 60, 'overdue_reminders': True, 'internal_question_expiry_days': 7, + 'goal_auto_review_enabled': False, 'goal_duplicate_auto_judge_enabled': False} response = client.post('/admin/api/goals', json={'content': '管理员安排', 'entry_id': 'A', 'deadline': '2026-10-09T05:00:00Z'}) assert response.status_code == 201, response.text @@ -130,7 +131,8 @@ def test_admin_goal_crud_filters_settings_notifications_and_audit(client, store) settings = client.patch('/admin/api/settings/goals', json={'default_reminder_minutes': 30, 'overdue_reminders': False}) assert settings.status_code == 200 - assert settings.json()['goals'] == {'default_reminder_minutes': 30, 'overdue_reminders': False, 'internal_question_expiry_days': 7} + assert settings.json()['goals'] == {'default_reminder_minutes': 30, 'overdue_reminders': False, 'internal_question_expiry_days': 7, + 'goal_auto_review_enabled': False, 'goal_duplicate_auto_judge_enabled': False} assert client.patch('/admin/api/settings/goals', json={}).json()['goals'] == settings.json()['goals'] with store.read() as conn: operations = [dict(row) for row in conn.execute("SELECT * FROM admin_operations WHERE object_type IN ('goal','settings')")] diff --git a/tests/test_media_retention.py b/tests/test_media_retention.py index 5c2ad186..6cb3c172 100644 --- a/tests/test_media_retention.py +++ b/tests/test_media_retention.py @@ -302,8 +302,15 @@ def old_format(data, manifest): with sqlite3.connect(database) as conn: columns = {r[1] for r in conn.execute('PRAGMA table_info(media_files)')} if 'bytes_purged_at' in columns: - # Migrations 038, 037 and 036 are newer than the columns removed below; undo them first. + # Migrations 039, 038, 037 and 036 are newer than the columns removed below; undo them first. # 038 only changes a setting value, so dropping its record is enough. + assert manifest['migrations'][-1] == '039_goal_auto_review.sql' + conn.executescript('''DROP INDEX messages_sender_entry; + DROP TABLE goal_auto_review_evidence; + DROP TABLE goal_auto_reviews; + DROP TABLE goal_duplicate_judgments; + ALTER TABLE goals DROP COLUMN auto_review_after_message_id;''') + conn.execute('DELETE FROM schema_migrations WHERE version=?', (manifest['migrations'].pop(),)) assert manifest['migrations'][-1] == '038_consolidation_default_auto.sql' conn.execute('DELETE FROM schema_migrations WHERE version=?', (manifest['migrations'].pop(),)) conn.executescript('''DROP TRIGGER consolidation_no_resurrection; diff --git a/tests/test_question_expiry_settings.py b/tests/test_question_expiry_settings.py index 393b9040..675da6cf 100644 --- a/tests/test_question_expiry_settings.py +++ b/tests/test_question_expiry_settings.py @@ -7,7 +7,7 @@ from conftest import login_admin from iris.api import create_app -from iris.goals import Goals, question_expiry_days +from iris.goals import AUTO_DEFAULTS, Goals, question_expiry_days KEY = 'internal_question_expiry_days' @@ -40,7 +40,7 @@ def test_get_missing_expiry_uses_default_without_writing(client, store, stored): response = client.get('/admin/api/settings') assert response.status_code == 200 assert response.json()['goals'] == { - 'default_reminder_minutes': 60, 'overdue_reminders': True, **(stored or {}), KEY: 7} + 'default_reminder_minutes': 60, 'overdue_reminders': True, **(stored or {}), KEY: 7, **AUTO_DEFAULTS} assert store.setting('goals') == stored assert operations(store) == before @@ -62,7 +62,7 @@ def test_patch_expiry_alone_preserves_reminder_settings(client, store, days): response = client.patch(PATH, json={KEY: days}) assert response.status_code == 200 expected = {'default_reminder_minutes': 90, 'overdue_reminders': False, KEY: days} - assert response.json()['goals'] == expected + assert response.json()['goals'] == {**expected, **AUTO_DEFAULTS} assert store.setting('goals') == expected with store.read() as conn: assert question_expiry_days(conn) == days @@ -79,9 +79,9 @@ def test_patch_combined_settings_returns_saved_values_and_one_audit(client, stor changes = {'default_reminder_minutes': 30, 'overdue_reminders': True, KEY: 14} response = client.patch(PATH, json=changes) assert response.status_code == 200 - assert response.json()['goals'] == changes + assert response.json()['goals'] == {**changes, **AUTO_DEFAULTS} assert store.setting('goals') == changes - assert client.get('/admin/api/settings').json()['goals'] == changes + assert client.get('/admin/api/settings').json()['goals'] == {**changes, **AUTO_DEFAULTS} after = operations(store) assert len(after) == len(before) + 1 assert after[-1]['actor'] == 'admin' diff --git a/tests/test_question_goal_expiry.py b/tests/test_question_goal_expiry.py index e15435ef..9ce6065a 100644 --- a/tests/test_question_goal_expiry.py +++ b/tests/test_question_goal_expiry.py @@ -280,7 +280,8 @@ def test_setting_write_persists_and_existing_admin_patch_preserves_internal_key( login_admin(client) response = client.patch('/admin/api/settings/goals', json={'default_reminder_minutes': 90, 'overdue_reminders': False}) assert response.status_code == 200 - assert response.json()['goals'] == {'default_reminder_minutes': 90, 'overdue_reminders': False, KEY: days} + assert response.json()['goals'] == {'default_reminder_minutes': 90, 'overdue_reminders': False, KEY: days, + 'goal_auto_review_enabled': False, 'goal_duplicate_auto_judge_enabled': False} assert client.patch('/admin/api/settings/goals', json={KEY: days}).status_code == 200 with store.read() as conn: assert question_expiry_days(conn) == days