Files
code-utils/cloudbind.go

281 lines
8.2 KiB
Go
Raw Normal View History

2026-08-14 07:52:01 +08:00
package main
// cloudbind.go 实现“项目身份与机器路径分离”的同步支撑:
// - machineID本机稳定标识哈希后缓存云端行名 paths:<machineID>
// - machinePathsDoc / applyMachinePaths只推拉本机的 {项目名:路径} 行
// - 云端有身份但本机没路径的项目进入待绑定清单,由 Dashboard 引导用户选目录绑定。
import (
"crypto/sha256"
"database/sql"
"encoding/hex"
"encoding/json"
"errors"
"os"
"strings"
)
// machineID 返回本机稳定标识(原始 ID 哈希后取 16 个十六进制字符),缓存于 meta。
func (a *App) machineID() string {
if a.store == nil {
return "unknown"
}
if v := a.store.Meta("machine_id"); v != "" {
return v
}
raw := rawMachineID()
if raw == "" {
raw, _ = os.Hostname()
}
if raw == "" {
raw = "unknown-machine"
}
sum := sha256.Sum256([]byte(strings.ToLower(strings.TrimSpace(raw))))
id := hex.EncodeToString(sum[:8])
_ = a.store.SetMeta("machine_id", id)
return id
}
// machinePathsDoc 生成本机路径文档 {项目名: 本机路径}map 序列化按键排序,内容稳定可比较)。
func (a *App) machinePathsDoc() (string, error) {
rows, e := a.store.db.Query(`SELECT name,path FROM projects ORDER BY name`)
if e != nil {
return "", e
}
defer rows.Close()
m := map[string]string{}
for rows.Next() {
var name, path string
if e := rows.Scan(&name, &path); e != nil {
return "", e
}
if name != "" && path != "" {
m[name] = path
}
}
b, e := json.Marshal(m)
return string(b), e
}
// applyMachinePaths 套用本机路径文档(换库/重装同机场景):
// 本地没有该名项目且路径非空 → 直接入库;已有同名项目但路径不同 → 以远端为准更新。
func (a *App) applyMachinePaths(doc string) error {
var m map[string]string
if json.Unmarshal([]byte(doc), &m) != nil {
return nil // 远端数据异常时忽略,不阻断同步
}
for name, path := range m {
name, path = strings.TrimSpace(name), strings.TrimSpace(path)
if name == "" || path == "" {
continue
}
var pid int64
var cur string
e := a.store.db.QueryRow(`SELECT id,path FROM projects WHERE name=? LIMIT 1`, name).Scan(&pid, &cur)
switch {
case e == sql.ErrNoRows:
now := nowRFC()
// path 全局唯一同路径已有别名项目时不强插IGNORE
if _, e := a.store.db.Exec(`INSERT OR IGNORE INTO projects(name,path,description,group_id,created_at,updated_at) VALUES(?,?,'',1,?,?)`,
name, path, now, now); e != nil {
return e
}
case e != nil:
return e
case cur != path:
if _, e := a.store.db.Exec(`UPDATE projects SET path=?,updated_at=? WHERE id=?
AND NOT EXISTS(SELECT 1 FROM projects p2 WHERE p2.path=? AND p2.id!=?)`,
path, nowRFC(), pid, path, pid); e != nil {
return e
}
}
}
return nil
}
// rebuildCloudPending 以最近一次云端身份文档为基准,重算仍未落地本机的项目清单(幂等)。
func (a *App) rebuildCloudPending(entries []projectDocEntry) {
pending := []projectDocEntry{}
for _, it := range entries {
name := strings.TrimSpace(it.Name)
if name == "" {
continue
}
var pid int64
if a.store.db.QueryRow(`SELECT id FROM projects WHERE name=? LIMIT 1`, name).Scan(&pid) == sql.ErrNoRows {
it.Path = "" // 待绑定清单不携带其它机器的路径
pending = append(pending, it)
}
}
b, _ := json.Marshal(pending)
_ = a.store.SetMeta("cloud_projects_pending", string(b))
}
1. 后台图表多样化 API 的 /admin/overview 新增了按 AI 提供商聚合的用量(providerSeries)和云端数据构成(dataDist)。Admin.vue 概览页改用 ECharts:日活折线图、Token 堆叠柱状图(叠加调用次数折线)、AI 提供商用量饼图、云端数据构成饼图。 2. 端口监控与刷新间隔 「存为应用」不再跳转启动台,而是在本页弹出复用的 LaunchAppFormModal 表单,保存成功后弹确认框询问「留在本页 / 前往启动台」。启动台和端口监控页头都加了 RefreshIntervalPicker(仅手动 / 5 / 10 / 30 / 60 秒,默认 30 秒),选择记忆在 localStorage。 3. 团队所有者与用户详情 团队列表的所有者、用户列表的用户名都改为「头像 + 昵称 + ID」的可点击单元格,点击弹出用户详情:头像、昵称、头衔、待办/工单/笔记/团队数量、注册时间、最近活跃、拥有团队数。API 新增 GET /admin/users/:id,团队列表关联查询出 ownerNickname/ownerAvatar。 4. 项目专属排除规则 SQLite exclusion_rules 加了 project_id 列(唯一约束改为 (project_id, pattern),旧库自动重建迁移)。项目详情页「代码」标签下新增专属规则面板,可增删;扫描时全局规则 + 项目规则叠加生效。规则随项目身份上云,其它机器认领项目后自动带回。 5. 一句话 AI 生成待办/工单 待办页和工单页工具栏各嵌入一个 AI 生成输入条:一句话回车后由后端 AIGenerateTasks 调用 AI 拆解成多条草稿,弹预览框逐条勾选、可改标题/类型/优先级/日期,选归属项目后批量保存。 6. 项目云同步与认领 全局挂载了 CloudClaimModal:登录同步后发现云端有本机未落地的项目时自动弹出(同一批只打扰一次),每个项目可「选目录认领」或「忽略」;工作台横幅和个人主页可随时再打开。 统计数据按机器码推送到 stats:<机器码> 文档,只推不拉,不同电脑的扫描历史互不覆盖。 个人主页 · 云同步新增「项目云同步」区块:自动/手动模式切换(手动模式下常规同步跳过项目文档,仅点「立即同步项目」时推拉)、待认领入口、已忽略项目的恢复列表。 途中补齐了约 60 个中英双语 i18n 键,并修掉了 PortMonitor 引用但缺失的 pmSavedToast 键。
2026-08-17 09:04:53 +08:00
// cloudIgnoredSet 已被用户忽略的云端项目名集合(认领弹窗里点了「忽略」)。
func (a *App) cloudIgnoredSet() map[string]bool {
out := map[string]bool{}
raw := a.store.Meta("cloud_projects_ignored")
if raw == "" {
return out
}
var names []string
if json.Unmarshal([]byte(raw), &names) != nil {
return out
}
for _, n := range names {
if n = strings.TrimSpace(n); n != "" {
out[n] = true
}
}
return out
}
func (a *App) saveCloudIgnored(set map[string]bool) {
names := make([]string, 0, len(set))
for n, on := range set {
if on {
names = append(names, n)
}
}
b, _ := json.Marshal(names)
_ = a.store.SetMeta("cloud_projects_ignored", string(b))
}
// IgnoreCloudProject 忽略某个云端待认领项目(仅本机记忆,可随时恢复)。
func (a *App) IgnoreCloudProject(name string) error {
if e := a.ready(); e != nil {
return e
}
name = strings.TrimSpace(name)
if name == "" {
return errors.New("NAME_REQUIRED")
}
set := a.cloudIgnoredSet()
set[name] = true
a.saveCloudIgnored(set)
return nil
}
// UnignoreCloudProject 恢复被忽略的云端项目,使其重新出现在待认领清单。
func (a *App) UnignoreCloudProject(name string) error {
if e := a.ready(); e != nil {
return e
}
set := a.cloudIgnoredSet()
delete(set, strings.TrimSpace(name))
a.saveCloudIgnored(set)
return nil
}
// ListCloudIgnoredProjects 返回被忽略的云端项目名(供认领弹窗「已忽略」区展示)。
func (a *App) ListCloudIgnoredProjects() ([]string, error) {
if e := a.ready(); e != nil {
return nil, e
}
out := []string{}
for n := range a.cloudIgnoredSet() {
out = append(out, n)
}
return out, nil
}
// ListCloudPendingProjects 返回云端存在但本机尚未绑定路径、且未被忽略的项目。
2026-08-14 07:52:01 +08:00
func (a *App) ListCloudPendingProjects() ([]projectDocEntry, error) {
if e := a.ready(); e != nil {
return nil, e
}
out := []projectDocEntry{}
raw := a.store.Meta("cloud_projects_pending")
if raw == "" {
return out, nil
}
var list []projectDocEntry
if json.Unmarshal([]byte(raw), &list) != nil {
return out, nil
}
1. 后台图表多样化 API 的 /admin/overview 新增了按 AI 提供商聚合的用量(providerSeries)和云端数据构成(dataDist)。Admin.vue 概览页改用 ECharts:日活折线图、Token 堆叠柱状图(叠加调用次数折线)、AI 提供商用量饼图、云端数据构成饼图。 2. 端口监控与刷新间隔 「存为应用」不再跳转启动台,而是在本页弹出复用的 LaunchAppFormModal 表单,保存成功后弹确认框询问「留在本页 / 前往启动台」。启动台和端口监控页头都加了 RefreshIntervalPicker(仅手动 / 5 / 10 / 30 / 60 秒,默认 30 秒),选择记忆在 localStorage。 3. 团队所有者与用户详情 团队列表的所有者、用户列表的用户名都改为「头像 + 昵称 + ID」的可点击单元格,点击弹出用户详情:头像、昵称、头衔、待办/工单/笔记/团队数量、注册时间、最近活跃、拥有团队数。API 新增 GET /admin/users/:id,团队列表关联查询出 ownerNickname/ownerAvatar。 4. 项目专属排除规则 SQLite exclusion_rules 加了 project_id 列(唯一约束改为 (project_id, pattern),旧库自动重建迁移)。项目详情页「代码」标签下新增专属规则面板,可增删;扫描时全局规则 + 项目规则叠加生效。规则随项目身份上云,其它机器认领项目后自动带回。 5. 一句话 AI 生成待办/工单 待办页和工单页工具栏各嵌入一个 AI 生成输入条:一句话回车后由后端 AIGenerateTasks 调用 AI 拆解成多条草稿,弹预览框逐条勾选、可改标题/类型/优先级/日期,选归属项目后批量保存。 6. 项目云同步与认领 全局挂载了 CloudClaimModal:登录同步后发现云端有本机未落地的项目时自动弹出(同一批只打扰一次),每个项目可「选目录认领」或「忽略」;工作台横幅和个人主页可随时再打开。 统计数据按机器码推送到 stats:<机器码> 文档,只推不拉,不同电脑的扫描历史互不覆盖。 个人主页 · 云同步新增「项目云同步」区块:自动/手动模式切换(手动模式下常规同步跳过项目文档,仅点「立即同步项目」时推拉)、待认领入口、已忽略项目的恢复列表。 途中补齐了约 60 个中英双语 i18n 键,并修掉了 PortMonitor 引用但缺失的 pmSavedToast 键。
2026-08-17 09:04:53 +08:00
ignored := a.cloudIgnoredSet()
2026-08-14 07:52:01 +08:00
for _, it := range list {
1. 后台图表多样化 API 的 /admin/overview 新增了按 AI 提供商聚合的用量(providerSeries)和云端数据构成(dataDist)。Admin.vue 概览页改用 ECharts:日活折线图、Token 堆叠柱状图(叠加调用次数折线)、AI 提供商用量饼图、云端数据构成饼图。 2. 端口监控与刷新间隔 「存为应用」不再跳转启动台,而是在本页弹出复用的 LaunchAppFormModal 表单,保存成功后弹确认框询问「留在本页 / 前往启动台」。启动台和端口监控页头都加了 RefreshIntervalPicker(仅手动 / 5 / 10 / 30 / 60 秒,默认 30 秒),选择记忆在 localStorage。 3. 团队所有者与用户详情 团队列表的所有者、用户列表的用户名都改为「头像 + 昵称 + ID」的可点击单元格,点击弹出用户详情:头像、昵称、头衔、待办/工单/笔记/团队数量、注册时间、最近活跃、拥有团队数。API 新增 GET /admin/users/:id,团队列表关联查询出 ownerNickname/ownerAvatar。 4. 项目专属排除规则 SQLite exclusion_rules 加了 project_id 列(唯一约束改为 (project_id, pattern),旧库自动重建迁移)。项目详情页「代码」标签下新增专属规则面板,可增删;扫描时全局规则 + 项目规则叠加生效。规则随项目身份上云,其它机器认领项目后自动带回。 5. 一句话 AI 生成待办/工单 待办页和工单页工具栏各嵌入一个 AI 生成输入条:一句话回车后由后端 AIGenerateTasks 调用 AI 拆解成多条草稿,弹预览框逐条勾选、可改标题/类型/优先级/日期,选归属项目后批量保存。 6. 项目云同步与认领 全局挂载了 CloudClaimModal:登录同步后发现云端有本机未落地的项目时自动弹出(同一批只打扰一次),每个项目可「选目录认领」或「忽略」;工作台横幅和个人主页可随时再打开。 统计数据按机器码推送到 stats:<机器码> 文档,只推不拉,不同电脑的扫描历史互不覆盖。 个人主页 · 云同步新增「项目云同步」区块:自动/手动模式切换(手动模式下常规同步跳过项目文档,仅点「立即同步项目」时推拉)、待认领入口、已忽略项目的恢复列表。 途中补齐了约 60 个中英双语 i18n 键,并修掉了 PortMonitor 引用但缺失的 pmSavedToast 键。
2026-08-17 09:04:53 +08:00
if ignored[strings.TrimSpace(it.Name)] {
continue
}
2026-08-14 07:52:01 +08:00
// 本地可能在同步间隙手动建了同名项目,再过滤一遍
var pid int64
if a.store.db.QueryRow(`SELECT id FROM projects WHERE name=? LIMIT 1`, it.Name).Scan(&pid) == sql.ErrNoRows {
out = append(out, it)
}
}
return out, nil
}
// BindCloudProject 把云端项目绑定到本机目录:带身份信息入库、移出待绑定清单,并触发一轮推送。
func (a *App) BindCloudProject(name, path string) (Project, error) {
if e := a.ready(); e != nil {
return Project{}, e
}
name, path = strings.TrimSpace(name), strings.TrimSpace(path)
if name == "" {
return Project{}, errors.New("NAME_REQUIRED")
}
if fi, e := os.Stat(path); e != nil || !fi.IsDir() {
return Project{}, errors.New("DIR_NOT_FOUND")
}
entry := projectDocEntry{Name: name}
if raw := a.store.Meta("cloud_projects_pending"); raw != "" {
var list []projectDocEntry
if json.Unmarshal([]byte(raw), &list) == nil {
rest := make([]projectDocEntry, 0, len(list))
for _, it := range list {
if strings.TrimSpace(it.Name) == name {
entry = it
continue
}
rest = append(rest, it)
}
b, _ := json.Marshal(rest)
_ = a.store.SetMeta("cloud_projects_pending", string(b))
}
}
gid, e := a.ensureGroupID(entry.Group)
if e != nil {
return Project{}, e
}
var pid int64
now := nowRFC()
switch e := a.store.db.QueryRow(`SELECT id FROM projects WHERE path=?`, path).Scan(&pid); {
case e == sql.ErrNoRows:
r, e2 := a.store.db.Exec(`INSERT INTO projects(name,path,description,group_id,created_at,updated_at) VALUES(?,?,?,?,?,?)`,
name, path, entry.Description, gid, now, now)
if e2 != nil {
return Project{}, e2
}
pid, _ = r.LastInsertId()
case e != nil:
return Project{}, e
default:
// 该目录已是本地项目:改名并入云端身份,避免出现两个同路径项目。
if _, e := a.store.db.Exec(`UPDATE projects SET name=?,description=?,group_id=?,updated_at=? WHERE id=?`,
name, entry.Description, gid, now, pid); e != nil {
return Project{}, e
}
}
if entry.Favorite {
_, _ = a.store.db.Exec(`INSERT OR IGNORE INTO favorites(project_id,created_at) VALUES(?,?)`, pid, now)
}
1. 后台图表多样化 API 的 /admin/overview 新增了按 AI 提供商聚合的用量(providerSeries)和云端数据构成(dataDist)。Admin.vue 概览页改用 ECharts:日活折线图、Token 堆叠柱状图(叠加调用次数折线)、AI 提供商用量饼图、云端数据构成饼图。 2. 端口监控与刷新间隔 「存为应用」不再跳转启动台,而是在本页弹出复用的 LaunchAppFormModal 表单,保存成功后弹确认框询问「留在本页 / 前往启动台」。启动台和端口监控页头都加了 RefreshIntervalPicker(仅手动 / 5 / 10 / 30 / 60 秒,默认 30 秒),选择记忆在 localStorage。 3. 团队所有者与用户详情 团队列表的所有者、用户列表的用户名都改为「头像 + 昵称 + ID」的可点击单元格,点击弹出用户详情:头像、昵称、头衔、待办/工单/笔记/团队数量、注册时间、最近活跃、拥有团队数。API 新增 GET /admin/users/:id,团队列表关联查询出 ownerNickname/ownerAvatar。 4. 项目专属排除规则 SQLite exclusion_rules 加了 project_id 列(唯一约束改为 (project_id, pattern),旧库自动重建迁移)。项目详情页「代码」标签下新增专属规则面板,可增删;扫描时全局规则 + 项目规则叠加生效。规则随项目身份上云,其它机器认领项目后自动带回。 5. 一句话 AI 生成待办/工单 待办页和工单页工具栏各嵌入一个 AI 生成输入条:一句话回车后由后端 AIGenerateTasks 调用 AI 拆解成多条草稿,弹预览框逐条勾选、可改标题/类型/优先级/日期,选归属项目后批量保存。 6. 项目云同步与认领 全局挂载了 CloudClaimModal:登录同步后发现云端有本机未落地的项目时自动弹出(同一批只打扰一次),每个项目可「选目录认领」或「忽略」;工作台横幅和个人主页可随时再打开。 统计数据按机器码推送到 stats:<机器码> 文档,只推不拉,不同电脑的扫描历史互不覆盖。 个人主页 · 云同步新增「项目云同步」区块:自动/手动模式切换(手动模式下常规同步跳过项目文档,仅点「立即同步项目」时推拉)、待认领入口、已忽略项目的恢复列表。 途中补齐了约 60 个中英双语 i18n 键,并修掉了 PortMonitor 引用但缺失的 pmSavedToast 键。
2026-08-17 09:04:53 +08:00
// 云端身份携带的项目级排除规则一并落地
if len(entry.Rules) > 0 {
if e := a.applyProjectRules(pid, entry.Rules); e != nil {
return Project{}, e
}
}
// 认领后如仍在忽略清单里,顺手移除
if set := a.cloudIgnoredSet(); set[name] {
delete(set, name)
a.saveCloudIgnored(set)
}
2026-08-14 07:52:01 +08:00
a.store.Log("info", "同步", "云端项目已绑定本机目录", name+" → "+path)
if a.syncUserID() > 0 {
go a.syncOnce(true)
}
return a.store.GetProject(pid)
}