Local branch had diverged from origin/master (sibling commits on the same base). Rewrote local history linearly on top of origin/master, folding in all local content: exa/hot-platform discovery hunters, leaks ledger, nightly sweep orchestrator, updated .gitignore and skill docs, plus the latest hunt output and vault state. Remote-only files (vault keys, channel scripts) were restored rather than dropped, so the resulting tree is a full union of both sides.
1 line
15 KiB
JSON
1 line
15 KiB
JSON
{"repo": "apflu/aichatgroup", "files": {".env.example": {"1ab7e46cba4893940cc7d4ff9f0e95a9edd5bca9": {"t": 1785745173.8271205, "n": 2032, "h": "6634b9e722341de9", "c": "# Anthropic API key —— 仅 --live 模式需要;replay_demo 默认用 MockGateway,无需 key\nANTHROPIC_API_KEY=sk-ant-...\n\n# ---- 模型选择:写成 provider_alias#model(别名与模型名解耦,见 gateway/router.py)----\n# 内置别名(配了对应标准 key 即可用):\n# anthropic#claude-opus-4-8 openai#gpt-4o gemini#gemini-2.0-flash\n# 不带 # 的裸模型名仍按前缀推断路由(向后兼容),但推荐显式写别名。\n\n# 其他 provider 的标准 key:\n# OPENAI_API_KEY=sk-...\n# OPENAI_BASE_URL=https://api.openai.com/v1 # 让内置 openai 别名指向兼容端点时改这里\n# GEMINI_API_KEY=... # 或 GOOGLE_API_KEY\n\n# 额外命名 provider —— 两种写法,任选:\n#\n# 写法 A(推荐,声明式):写一个 providers.json(见 examples/providers.example.json),\n# 每个 provider 一个块:{ \"alias\", \"type\", \"provider_url\", \"api_key_env\" }。\n# 放在项目根会被自动加载;或用 AICG_PROVIDERS_FILE 指定路径。密钥仍留在下面这些 *_env 里。\n# AICG_PROVIDERS_FILE=providers.json\n# DEEPSEEK_API_KEY=sk-...\n# GROQ_API_KEY=gsk-...\n#\n# 写法 B(摊平进 env,无需额外文件):\n# AICG_PROVIDERS=deepseek,groq\n# AICG_PROVIDER_DEEPSEEK_KIND=openai # openai | anthropic | gemini(默认 openai)\n# AICG_PROVIDER_DEEPSEEK_BASE_URL=https://api.deepseek.com/v1\n# AICG_PROVIDER_DEEPSEEK_API_KEY=sk-... # 或 AICG_PROVIDER_DEEPSEEK_API_KEY_ENV=DEEPSEEK_KEY\n#\n# 两种写法都让你之后在角色卡 / 调度模型里写 deepseek#deepseek-chat\n\n# 可选:覆盖各层默认模型(推荐用别名#模型 形式)\n# AICG_MODEL_OPUS=anthropic#claude-opus-4-8\n# AICG_MODEL_SONNET=anthropic#claude-sonnet-5\n# AICG_MODEL_HAIKU=anthropic#claude-haiku-4-5\n# AICG_MODEL_DIRECTOR=anthropic#claude-haiku-4-5 # 调度用便宜模型\n# AICG_MODEL_COMPACTION=anthropic#claude-haiku-4-5 # 压缩用便宜模型\n\n# 可选:主循环节奏与 compaction 阈值\n# AICG_TURN_INTERVAL_S=1.5\n# AICG_IDLE_POLL_S=2.0\n# AICG_MAX_HISTORY=60\n# AICG_KEEP_LAST=20\n# AICG_SQLITE_PATH=aichatgroup.sqlite\n# AICG_PRESET_PATH=examples/room.example.json\n\n# ---- M1 Telegram 落地(scripts/run_telegram.py 需要)----\n# 观察者 bot(务必在 BotFather 关掉 privacy mode)+ 群 chat_id\nTG_TOKEN_OBSERVER=123456:观察者bot-token\nTG_CHAT_ID=-1001234567890\n# 每个角色一个 bot token(与预设 JSON 里的 bot_token_env 对应,按槽位编号、不绑角色名)\nTG_TOKEN_CHAR_1=123456:角色1bot-token\nTG_TOKEN_CHAR_2=123456:角色2bot-token\nTG_TOKEN_CHAR_3=123456:角色3bot-token\n"}, "0bd0b180954e5efd24a0dc36cda2c9b8ba2164a6": {"t": 1785748390.632067, "n": 2032, "h": "6634b9e722341de9", "c": "# Anthropic API key —— 仅 --live 模式需要;replay_demo 默认用 MockGateway,无需 key\nANTHROPIC_API_KEY=sk-ant-...\n\n# ---- 模型选择:写成 provider_alias#model(别名与模型名解耦,见 gateway/router.py)----\n# 内置别名(配了对应标准 key 即可用):\n# anthropic#claude-opus-4-8 openai#gpt-4o gemini#gemini-2.0-flash\n# 不带 # 的裸模型名仍按前缀推断路由(向后兼容),但推荐显式写别名。\n\n# 其他 provider 的标准 key:\n# OPENAI_API_KEY=sk-...\n# OPENAI_BASE_URL=https://api.openai.com/v1 # 让内置 openai 别名指向兼容端点时改这里\n# GEMINI_API_KEY=... # 或 GOOGLE_API_KEY\n\n# 额外命名 provider —— 两种写法,任选:\n#\n# 写法 A(推荐,声明式):写一个 providers.json(见 examples/providers.example.json),\n# 每个 provider 一个块:{ \"alias\", \"type\", \"provider_url\", \"api_key_env\" }。\n# 放在项目根会被自动加载;或用 AICG_PROVIDERS_FILE 指定路径。密钥仍留在下面这些 *_env 里。\n# AICG_PROVIDERS_FILE=providers.json\n# DEEPSEEK_API_KEY=sk-...\n# GROQ_API_KEY=gsk-...\n#\n# 写法 B(摊平进 env,无需额外文件):\n# AICG_PROVIDERS=deepseek,groq\n# AICG_PROVIDER_DEEPSEEK_KIND=openai # openai | anthropic | gemini(默认 openai)\n# AICG_PROVIDER_DEEPSEEK_BASE_URL=https://api.deepseek.com/v1\n# AICG_PROVIDER_DEEPSEEK_API_KEY=sk-... # 或 AICG_PROVIDER_DEEPSEEK_API_KEY_ENV=DEEPSEEK_KEY\n#\n# 两种写法都让你之后在角色卡 / 调度模型里写 deepseek#deepseek-chat\n\n# 可选:覆盖各层默认模型(推荐用别名#模型 形式)\n# AICG_MODEL_OPUS=anthropic#claude-opus-4-8\n# AICG_MODEL_SONNET=anthropic#claude-sonnet-5\n# AICG_MODEL_HAIKU=anthropic#claude-haiku-4-5\n# AICG_MODEL_DIRECTOR=anthropic#claude-haiku-4-5 # 调度用便宜模型\n# AICG_MODEL_COMPACTION=anthropic#claude-haiku-4-5 # 压缩用便宜模型\n\n# 可选:主循环节奏与 compaction 阈值\n# AICG_TURN_INTERVAL_S=1.5\n# AICG_IDLE_POLL_S=2.0\n# AICG_MAX_HISTORY=60\n# AICG_KEEP_LAST=20\n# AICG_SQLITE_PATH=aichatgroup.sqlite\n# AICG_PRESET_PATH=examples/room.example.json\n\n# ---- M1 Telegram 落地(scripts/run_telegram.py 需要)----\n# 观察者 bot(务必在 BotFather 关掉 privacy mode)+ 群 chat_id\nTG_TOKEN_OBSERVER=123456:观察者bot-token\nTG_CHAT_ID=-1001234567890\n# 每个角色一个 bot token(与预设 JSON 里的 bot_token_env 对应,按槽位编号、不绑角色名)\nTG_TOKEN_CHAR_1=123456:角色1bot-token\nTG_TOKEN_CHAR_2=123456:角色2bot-token\nTG_TOKEN_CHAR_3=123456:角色3bot-token\n"}, "29aaeb1fa7a89e2b34163d54a482af1dff06339f": {"t": 1785748391.741618, "n": 235, "h": "edae7f03516e10f3", "c": "# Anthropic API key —— 仅 --live 模式需要;replay_demo 默认用 MockGateway,无需 key\nANTHROPIC_API_KEY=sk-ant-...\n\n# 可选:覆盖各层默认模型(见 config.py)\n# AICG_MODEL_OPUS=claude-opus-4-8\n# AICG_MODEL_SONNET=claude-sonnet-5\n# AICG_MODEL_HAIKU=claude-haiku-4-5\n"}}, "src/aichatgroup/config.py": {"1ab7e46cba4893940cc7d4ff9f0e95a9edd5bca9": {"t": 1786068521.9160078, "n": 7196, "h": "1a91315917aee76c", "c": "\"\"\"运行配置与默认模型。\n\n模型 ID 以 Anthropic 官方现役别名为准(截至 2026-07):\n- Opus 4.8 : claude-opus-4-8\n- Sonnet 5 : claude-sonnet-5\n- Haiku 4.5: claude-haiku-4-5\n\"\"\"\nfrom __future__ import annotations\n\nimport json\nimport os\nimport re\nfrom dataclasses import dataclass, field\nfrom pathlib import Path\n\n# 各层默认模型别名(可被环境变量覆盖)\nDEFAULT_MODEL_OPUS = \"claude-opus-4-8\"\nDEFAULT_MODEL_SONNET = \"claude-sonnet-5\"\nDEFAULT_MODEL_HAIKU = \"claude-haiku-4-5\"\n\n\n@dataclass\nclass ProviderSpec:\n \"\"\"一个可配置的 provider 别名 → 具体网关的定义。\n\n kind 决定用哪套适配器:openai(含所有兼容端点)/ anthropic / gemini。\n 内置别名 anthropic/openai/gemini 由标准 key 自动装配;这里是**额外**的命名端点,\n 比如多个 OpenAI 兼容服务(deepseek / groq / 本地 vLLM)各自的 base_url + key。\n \"\"\"\n\n alias: str\n kind: str = \"openai\" # openai | anthropic | gemini\n base_url: str | None = None\n api_key: str | None = None\n\n @classmethod\n def from_dict(cls, d: dict) -> \"ProviderSpec\":\n \"\"\"从声明式配置构造。字段:\n\n alias 必填,provider 别名(用 别名#模型 引用)\n type / kind openai | anthropic | gemini(默认 openai)\n provider_url / base_url / url 端点地址(openai 兼容端点必填)\n api_key 直接写密钥(不推荐,会进配置文件)\n api_key_env 密钥所在的环境变量名(推荐,密钥留在 .env 里)\n \"\"\"\n alias = d.get(\"alias\") or d.get(\"name\")\n if not alias:\n raise ValueError(f\"provider 定义缺少 alias:{d!r}\")\n api_key = d.get(\"api_key\")\n if not api_key and d.get(\"api_key_env\"):\n api_key = os.environ.get(d[\"api_key_env\"])\n return cls(\n alias=str(alias),\n kind=str(d.get(\"type\") or d.get(\"kind\") or \"openai\").lower(),\n base_url=d.get(\"provider_url\") or d.get(\"base_url\") or d.get(\"url\"),\n api_key=api_key,\n )\n\n\ndef load_provider_specs(path: str | os.PathLike[str]) -> list[ProviderSpec]:\n \"\"\"从 JSON 文件读 provider 列表。\n\n 文件可以是 {\"providers\": [ {...}, {...} ]} 或直接一个 [ {...}, {...} ] 数组。\n \"\"\"\n data = json.loads(Path(path).read_text(encoding=\"utf-8\"))\n items = data.get(\"providers\", []) if isinstance(data, dict) else data\n return [ProviderSpec.from_dict(x) for x in items]\n\n\ndef _parse_providers() -> list[ProviderSpec]:\n \"\"\"从 env 读额外 provider 定义。\n\n AICG_PROVIDERS=deepseek,groq\n AICG_PROVIDER_DEEPSEEK_KIND=openai\n AICG_PROVIDER_DEEPSEEK_BASE_URL=https://api.deepseek.com/v1\n AICG_PROVIDER_DEEPSEEK_API_KEY=sk-... # 或 *_API_KEY_ENV 间接指向另一个环境变量\n \"\"\"\n raw = os.environ.get(\"AICG_PROVIDERS\", \"\")\n specs: list[ProviderSpec] = []\n for alias in (a.strip() for a in raw.split(\",\") if a.strip()):\n up = alias.upper()\n key = os.environ.get(f\"AICG_PROVIDER_{up}_API_KEY\")\n if not key:\n key_env = os.environ.get(f\"AICG_PROVIDER_{up}_API_KEY_ENV\")\n key = os.environ.get(key_env) if key_env else None\n specs.append(ProviderSpec(\n alias=alias,\n kind=os.environ.get(f\"AICG_PROVIDER_{up}_KIND\", \"openai\").lower(),\n base_url=os.environ.get(f\"AICG_PROVIDER_{up}_BASE_URL\"),\n api_key=key,\n ))\n return specs\n\n\ndef _strip_inline_comment(value: str) -> str:\n \"\"\"剥掉行内注释:`#` 前有空白才算注释起点。\n\n 值本身可能含 `#`(如 provider_alias#model),那种 `#` 前无空白,不会被误伤。\n 整段带引号的值(\"...\" / '...')则原样保留、不剥注释。\n \"\"\"\n v = value.strip()\n if v[:1] in (\"'\", '\"'):\n return v\n m = re.search(r\"\\s#\", v)\n return v[: m.start()].rstrip() if m else v\n\n\ndef _load_dotenv(path: Path) -> None:\n \"\"\"极简 .env 加载:仅在文件存在时把 KEY=VALUE 写入 os.environ(不覆盖已存在项)。\"\"\"\n if not path.is_file():\n return\n for raw in path.read_text(encoding=\"utf-8\").splitlines():\n line = raw.strip()\n if not line or line.startswith(\"#\") or \"=\" not in line:\n continue\n key, _, value = line.partition(\"=\")\n key = key.strip()\n value = _strip_inline_comment(value).strip('\"').strip(\"'\")\n os.environ.setdefault(key, value)\n\n\n@dataclass\nclass Settings:\n anthropic_api_key: str | None = None\n model_opus: str = DEFAULT_MODEL_OPUS\n model_sonnet: str = DEFAULT_MODEL_SONNET\n model_haiku: str = DEFAULT_MODEL_HAIKU\n # 单个发言回合默认输出上限(气泡短,无需很大)\n max_tokens: int = 1024\n\n # ---- 多 provider(可混用;模型写成 provider_alias#model,见 gateway/router.py)----\n openai_api_key: str | None = None\n openai_base_url: str | None = None # 内置 openai 别名指向兼容端点时设这个\n openai_prefixes: str = \"\" # 逗号分隔的额外前缀,供裸模型名的前缀推断\n gemini_api_key: str | None = None\n # 额外的命名 provider(别名可配置;用 alias#model 引用)\n providers: list = field(default_factory=list)\n\n # ---- M1 编排相关 ----\n # 调度与 compaction 用便宜模型(Haiku)\n director_model: str = DEFAULT_MODEL_HAIKU\n compaction_model: str = DEFAULT_MODEL_HAIKU\n # 主循环节奏(秒)\n turn_interval_s: float = 1.5 # 两个发言回合之间的间隔(也顺带遵守 Telegram 限速)\n idle_poll_s: float = 2.0 # 暂停 / 无人接话时的轮询间隔\n # compaction 阈值\n max_history: int = 60 # 历史超过这么多条触发压缩\n keep_last: int = 20 # 压缩后保留的近期条数\n # 持久化与预设\n sqlite_path: str = \"aichatgroup.sqlite\"\n preset_path: str | None = None\n\n @classmethod\n def from_env(cls, dotenv_path: str | os.PathLike[str] | None = None) -> \"Settings\":\n if dotenv_path is not None:\n _load_dotenv(Path(dotenv_path))\n else:\n _load_dotenv(Path.cwd() / \".env\")\n\n def _f(name: str, default: float) -> float:\n raw = os.environ.get(name)\n return float(raw) if raw else default\n\n def _i(name: str, default: int) -> int:\n raw = os.environ.get(name)\n return int(raw) if raw else default\n\n # provider 定义:声明式 JSON 文件 + 摊平的 env 变量,二者合并(同别名后者覆盖)\n providers = _parse_providers()\n providers_file = os.environ.get(\"AICG_PROVIDERS_FILE\")\n if providers_file is None and (Path.cwd() / \"providers.json\").is_file():\n providers_file = str(Path.cwd() / \"providers.json\")\n if providers_file and Path(providers_file).is_file():\n providers = load_provider_specs(providers_file) + providers\n\n return cls(\n anthropic_api_key=os.environ.get(\"ANTHROPIC_API_KEY\"),\n openai_api_key=os.environ.get(\"OPENAI_API_KEY\"),\n openai_base_url=os.environ.get(\"OPENAI_BASE_URL\"),\n openai_prefixes=os.environ.get(\"AICG_OPENAI_PREFIXES\", \"\"),\n gemini_api_key=os.environ.get(\"GEMINI_API_KEY\") or os.environ.get(\"GOOGLE_API_KEY\"),\n model_opus=os.environ.get(\"AICG_MODEL_OPUS\", DEFAULT_MODEL_OPUS),\n model_sonnet=os.environ.get(\"AICG_MODEL_SONNET\", DEFAULT_MODEL_SONNET),\n model_haiku=os.environ.get(\"AICG_MODEL_HAIKU\", DEFAULT_MODEL_HAIKU),\n director_model=os.environ.get(\"AICG_MODEL_DIRECTOR\", DEFAULT_MODEL_HAIKU),\n compaction_model=os.environ.get(\"AICG_MODEL_COMPACTION\", DEFAULT_MODEL_HAIKU),\n turn_interval_s=_f(\"AICG_TURN_INTERVAL_S\", 1.5),\n idle_poll_s=_f(\"AICG_IDLE_POLL_S\", 2.0),\n max_history=_i(\"AICG_MAX_HISTORY\", 60),\n keep_last=_i(\"AICG_KEEP_LAST\", 20),\n sqlite_path=os.environ.get(\"AICG_SQLITE_PATH\", \"aichatgroup.sqlite\"),\n preset_path=os.environ.get(\"AICG_PRESET_PATH\"),\n providers=providers,\n )\n"}}}} |