using System;
using System.Collections.Generic;
using System.IO;
using System.Net;
using System.Text;
namespace 成套报价软件
{
///
/// ZCode AI 引擎(zai-org/ZCode 开源版)连接管理(★2026-09-26 由 OpenClaw 换芯为 ZCode,
/// 类名保留 OpenClawHost 减少调用点改动)。网关由 ai助手\启动引擎.cmd 拉起(内部跑
/// zcode\bin\zcode.mjs --web --token …),本类只负责:预置补全、启动参数落盘、健康探测、
/// 令牌/端口管理、开机自启登记。
/// 运行时布局:引擎目录(\ai助手)\zcode\bin\zcode.mjs + zcode\node\node.exe(随包发行,Apache-2.0)
/// 数据目录:引擎目录\zcode数据(经环境变量 ZCODE_DATA_BASE_DIR 隔离——技能/配置/凭据全在里面,
/// 不碰用户自己的 ~/.zcode;删除该目录即完全重置 AI 助手)
/// 令牌目录:用户主目录\.zcode\chengtao\(token.txt+port.txt——技能脚本的固定读取路径)
/// 探测:GET http://127.0.0.1:端口/api/server-info?token=… 返回 200 即就绪。
/// ★模型选择完全用 ZCode 自带默认行为,本类不写任何模型选择/清单配置(用户 2026-09-26 要求)。
///
internal static class OpenClawHost
{
private const int 默认端口 = 18789;
private static readonly object _锁 = new object();
private static readonly object _日志锁 = new object();
private static string _令牌;
private static int _端口 = 默认端口;
private static bool _已加载配置;
/// 网关端口(引擎重启后生效)。
internal static int 端口 { get { return _端口; } }
/// 连接令牌(首跑生成并持久化,Web 面板经 URL ?token= 认证,技能回调主程序共用)。
internal static string 令牌 { get { 加载配置(); return _令牌; } }
/// 引擎目录(启动引擎.cmd 所在目录):优先 exe 旁(安装布局),
/// 开发机回退 项目根\ai助手(bin\Debug 起三层 ..)。
internal static string 引擎目录()
{
string exeDir = AppDomain.CurrentDomain.BaseDirectory;
string a = Path.Combine(exeDir, "ai助手");
if (File.Exists(Path.Combine(a, "启动引擎.cmd"))) return a;
try
{
// 开发机: ...\成套报价软件\成套报价软件\bin\Debug\ → 三层 .. 才是方案根
string[] devs = {
Path.GetFullPath(Path.Combine(exeDir, "..", "..", "..", "ai助手")),
Path.GetFullPath(Path.Combine(exeDir, "..", "..", "ai助手"))
};
foreach (string dev in devs)
{
if (File.Exists(Path.Combine(dev, "启动引擎.cmd"))) return dev;
}
}
catch { }
return a;
}
/// ZCode 运行时(发行包入口 + 捆绑 node)是否齐全。
internal static bool 运行时已安装()
{
string 根 = 引擎目录();
return File.Exists(Path.Combine(根, "zcode", "bin", "zcode.mjs"))
&& File.Exists(Path.Combine(根, "zcode", "node", "node.exe"));
}
/// 引擎数据目录(ZCODE_DATA_BASE_DIR 隔离;技能在 zcode数据\.zcode\skills、
/// provider/凭据在 zcode数据\.zcode\v2;与用户自己的 ~/.zcode 完全互不影响)。
internal static string 数据目录()
{
return Path.Combine(引擎目录(), "zcode数据");
}
/// 引擎状态目录(设置对话框"复制状态目录"用;= 数据目录)。
internal static string 状态目录()
{
return 数据目录();
}
/// 令牌/端口文件目录(用户主目录\.zcode\chengtao\,纯 ASCII 路径防 cmd 编码坑):
/// 技能脚本(xuanxing.js/SKILL.md)与 启动引擎.cmd 都从这里读固定路径,与安装位置无关。
private static string 令牌目录()
{
return Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.UserProfile),
".zcode", "chengtao");
}
/// 网关诊断日志(本类写连接/配置事件,引擎自身日志在 ai助手\引擎控制台.log)。
internal static string 网关日志路径()
{
return Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "logs", "ai助手_gateway.log");
}
/// 写一行诊断日志(DockWindow10 也复用)。
internal static void 写日志(string 文本)
{
try
{
lock (_日志锁)
{
string f = 网关日志路径();
string dir = Path.GetDirectoryName(f);
if (!string.IsNullOrEmpty(dir)) Directory.CreateDirectory(dir);
if (File.Exists(f) && new FileInfo(f).Length > 2 * 1024 * 1024)
File.WriteAllText(f, "", Encoding.UTF8); // 超过 2MB 清零重写,防无限膨胀
File.AppendAllText(f, DateTime.Now.ToString("HH:mm:ss.fff ") + 文本 + Environment.NewLine, Encoding.UTF8);
}
}
catch { }
}
// ---------- 配置(shezhi\AI助手.config.xml:软件侧端口/令牌登记) ----------
private static string 配置路径()
{
return Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "shezhi", "AI助手.config.xml");
}
private static void 加载配置()
{
lock (_锁)
{
if (_已加载配置) return;
_已加载配置 = true;
try
{
string f = 配置路径();
if (File.Exists(f))
{
string xml = File.ReadAllText(f, Encoding.UTF8);
_端口 = 提取XML值(xml, "端口", 默认端口);
_令牌 = 提取XML值(xml, "令牌", "");
}
}
catch { }
if (_端口 < 1024 || _端口 > 65535) _端口 = 默认端口;
if (string.IsNullOrEmpty(_令牌))
_令牌 = Guid.NewGuid().ToString("N") + Guid.NewGuid().ToString("N").Substring(0, 16);
保存配置();
}
}
private static void 保存配置()
{
try
{
string f = 配置路径();
string dir = Path.GetDirectoryName(f);
if (!string.IsNullOrEmpty(dir)) Directory.CreateDirectory(dir);
StringBuilder sb = new StringBuilder();
sb.AppendLine("");
sb.AppendLine("");
sb.AppendLine("");
sb.AppendLine(" <端口>" + _端口 + "端口>");
sb.AppendLine(" <令牌>" + _令牌 + "令牌>");
sb.AppendLine("");
File.WriteAllText(f, sb.ToString(), Encoding.UTF8);
}
catch { }
}
/// 设置对话框改端口用(保存到配置并刷新启动参数文件,重启引擎后生效)。
internal static void 保存端口(int 新端口)
{
加载配置();
lock (_锁)
{
if (新端口 >= 1024 && 新端口 <= 65535 && 新端口 != _端口)
{
_端口 = 新端口;
保存配置();
写启动参数();
}
}
}
private static int 提取XML值(string xml, string 标签, int 缺省)
{
string v = 提取XML值(xml, 标签, "");
int n;
if (int.TryParse(v, out n)) return n;
return 缺省;
}
private static string 提取XML值(string xml, string 标签, string 缺省)
{
if (string.IsNullOrEmpty(xml)) return 缺省;
string 开 = "<" + 标签 + ">", 关 = "" + 标签 + ">";
int i = xml.IndexOf(开, StringComparison.Ordinal);
if (i < 0) return 缺省;
int j = xml.IndexOf(关, i + 开.Length, StringComparison.Ordinal);
if (j < 0) return 缺省;
return xml.Substring(i + 开.Length, j - i - 开.Length).Trim();
}
// ---------- 启动参数落盘(令牌目录:令牌.txt + 端口.txt) ----------
/// 确保配置已就绪:预置补全 → 加载软件侧配置 → 把端口/令牌写进令牌目录
/// (技能与启动脚本读这里)。纯文件操作。
internal static void EnsureConfigured()
{
预置补全(); // ★ 新装机器:先补技能/provider配置/密钥(缺什么补什么,已有的绝不动)
加载配置();
写启动参数();
}
private static void 写启动参数()
{
try
{
// ★UTF8Encoding(false)=无 BOM:cmd 的 set /p 与 JSON.parse 都不接受 BOM 前缀
var utf8无BOM = new UTF8Encoding(false);
string tk = _令牌 ?? "";
string pt = (_端口 > 0 ? _端口 : 默认端口).ToString();
// 双写两处:真实用户目录(停止脚本/手动工具读) + 引擎数据目录(引擎进程 HOME/USERPROFILE
// 已被 启动引擎.cmd 指到数据目录——会话隔离后引擎内的 ~/.zcode/chengtao 即此路径)
string d1 = 令牌目录();
Directory.CreateDirectory(d1);
File.WriteAllText(Path.Combine(d1, "token.txt"), tk, utf8无BOM);
File.WriteAllText(Path.Combine(d1, "port.txt"), pt, utf8无BOM);
string d2 = Path.Combine(数据目录(), ".zcode", "chengtao");
try
{
Directory.CreateDirectory(d2);
File.WriteAllText(Path.Combine(d2, "token.txt"), tk, utf8无BOM);
File.WriteAllText(Path.Combine(d2, "port.txt"), pt, utf8无BOM);
}
catch { } // 数据目录写不进不阻塞(引擎侧还有真实目录兜底)
}
catch (Exception ex) { 写日志("[AI助手] 写启动参数失败: " + ex.Message); }
}
// ---------- 新装机器预置补全(ai助手\zcode预置 → 引擎数据目录) ----------
/// 把安装包自带的 AI 预置补装到引擎数据目录,缺什么补什么:
/// ① 技能(预置 skills\* 与 workshop-skills\* 全部 → zcode数据\workspace\.zcode\skills\)——
/// ★ZCode 技能根规则(roots.ts):用户级=~/.zcode(不吃数据目录隔离,装那会污染用户自己的
/// ZCode);项目级=工作区 cwd 的 .zcode\skills——引擎工作区固定为 zcode数据\workspace,
/// 技能装这里=随数据目录隔离+引擎可见。★技能带版本戳(zcode预置\技能版本.txt):
/// 版本变化时整份覆盖升级,否则只补缺目录;
/// ② 个人 provider 配置(zcode数据\.zcode\v2\provider_config.json):缺我们的 GLM 接入项时
/// 从 provider-模板.json 合并(anthropic-messages @ open.bigmodel.cn,GLM-5.3/GLM-5.3-Flash);
/// ③ zai密钥.txt 已填有效密钥且我们的接入项还没有 apiKey → 直接写入配置文件
/// (纯 JSON 文件操作,注册密钥.cmd 退役)。
/// ★模型选择完全不碰——用 ZCode 自带默认行为(用户 2026-09-26 要求)。
/// 用户自己的配置/密钥绝不动;技能为软件托管内容,随版本升级覆盖。
private static void 预置补全()
{
try
{
string 预置 = Path.Combine(引擎目录(), "zcode预置");
if (!Directory.Exists(预置)) return;
string 数据 = 数据目录();
string 技能根 = Path.Combine(数据, "workspace", ".zcode", "skills");
// ① 技能——带版本戳:缺目录补装,版本变化整份覆盖升级
// ★ 遍历预置 skills\* 与 workshop-skills\* 全部技能目录(ZCode 单一 skills 根,
// 两处预置都铺到同一目标):以后预置里加新技能自动装/升,不用改代码
string 预置版本文件 = Path.Combine(预置, "技能版本.txt");
string 已装版本文件 = Path.Combine(技能根, "dqb预置版本.txt");
string 预置版本 = File.Exists(预置版本文件)
? File.ReadAllText(预置版本文件, Encoding.UTF8).Trim() : "";
string 已装版本 = File.Exists(已装版本文件)
? File.ReadAllText(已装版本文件, Encoding.UTF8).Trim() : "";
bool 版本变了 = 预置版本.Length > 0 && 预置版本 != 已装版本;
var 技能对 = new List>();
string 预置技能根 = Path.Combine(预置, "skills");
if (Directory.Exists(预置技能根))
{
foreach (string d in Directory.GetDirectories(预置技能根))
技能对.Add(new KeyValuePair(
d, Path.Combine(技能根, Path.GetFileName(d))));
}
string 预置工作坊根 = Path.Combine(预置, "workshop-skills");
if (Directory.Exists(预置工作坊根))
{
foreach (string d in Directory.GetDirectories(预置工作坊根))
技能对.Add(new KeyValuePair(
d, Path.Combine(技能根, Path.GetFileName(d))));
}
if (版本变了)
{
// 覆盖升级:先删旧目录再整份拷(技能是软件托管内容,随包升级)
int 升级数 = 0;
foreach (var kv in 技能对)
{
覆盖技能目录(kv.Key, kv.Value);
升级数++;
}
try
{
Directory.CreateDirectory(Path.GetDirectoryName(已装版本文件));
File.WriteAllText(已装版本文件, 预置版本, Encoding.UTF8);
}
catch { }
写日志("[AI助手] 预置补全:已升级 " + 升级数 + " 个技能到版本 " + 预置版本);
}
else
{
int 补技能数 = 0;
foreach (var kv in 技能对)
{
if (补目录(kv.Key, kv.Value)) 补技能数++;
}
if (补技能数 > 0) 写日志("[AI助手] 预置补全:已安装 " + 补技能数 + " 个技能");
}
// ② 个人 provider 配置(缺项合并)+ ③ 密钥直写
合并个人Provider配置(预置, 数据);
// ④ 内置插件 seed(2026-09-26 用户报"旧版技能好多没生效"):会话隔离(HOME=数据目录)
// 后引擎看不到用户桌面版的插件缓存——开源仓库自带的两个官方内置插件
// (browser-use 浏览器自动化/node-repl-host)从预置种进隔离目录,离线可用;
// documents/pdf 等其余插件为桌面版专有(开源仓库无源码),CDN 社区市场另有 26 个可联网装。
Seed内置插件(预置, 数据);
}
catch (Exception ex) { 写日志("[AI助手] 预置补全异常: " + ex.Message); }
}
/// 内置插件 seed:预置\内置插件\<插件名>\(目录名=插件名,内含 .zcode-plugin\plugin.json)
/// → 数据目录\.zcode\cli\plugins\cache\zcode-plugins-official\<名>\<版本>\,
/// 并写 bundled-marketplace.json 分片与合并 marketplace.json(ZCode 原生 seed 格式)。
/// 幂等:cache 里已有该插件则跳过;纯文件操作,无需网络。
private static void Seed内置插件(string 预置, string 数据)
{
try
{
string 预置插件根 = Path.Combine(预置, "内置插件");
if (!Directory.Exists(预置插件根)) return;
string plugins根 = Path.Combine(数据, ".zcode", "cli", "plugins");
string cache根 = Path.Combine(plugins根, "cache", "zcode-plugins-official");
var 已种 = new List>();
foreach (string 源 in Directory.GetDirectories(预置插件根))
{
string 名 = Path.GetFileName(源);
string 清单 = Path.Combine(源, ".zcode-plugin", "plugin.json");
if (!File.Exists(清单)) continue;
// 版本从 plugin.json 读(读失败跳过该插件)
string 版本 = "";
try
{
var jp = new System.Web.Script.Serialization.JavaScriptSerializer();
object oP = jp.Deserialize