Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,10 @@ venv/
# Go test scratch directories
renameV1/tmp/

# Frontend dependencies and build outputs
web/**/node_modules/
web/**/dist/

# .NET build artifacts
**/bin/
**/obj/
Expand Down
3 changes: 3 additions & 0 deletions README.MD
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
./install.sh --tool renameV1 --tool check_keywords
./install.sh --tools retry_exec,codex_hook_notify
./install.sh --tool file_share
./install.sh --tool life_codex_server --tool life_codex_agent
```

自定义安装目录和配置目录:
Expand Down Expand Up @@ -58,6 +59,8 @@
| CLI | `file_share` | `cli/file_share/` | `file_share` | 临时启动只读 HTTP 文件分享服务 | [docs/cli/file_share.md](docs/cli/file_share.md) |
| CLI | `video_subtitle` | `cli/video_subtitle/` | `video_subtitle` | 为单个视频生成简体中文字幕 | [docs/cli/video_subtitle.md](docs/cli/video_subtitle.md) |
| Experimental CLI | `codex_inspector` | `cli/codex_inspector/` | `codex_inspector` | 本机只读查看 Codex 会话、token 用量、活跃度和记忆内容 | [docs/cli/codex_inspector.md](docs/cli/codex_inspector.md) |
| Experimental CLI | `life_codex_server` | `cli/life_codex_server/` | `life_codex_server` | 远程 Codex 控制台中心服务和网页 | [docs/cli/life_codex.md](docs/cli/life_codex.md) |
| Experimental CLI | `life_codex_agent` | `cli/life_codex_agent/` | `life_codex_agent` | 连接中心服务并执行本机 Codex app-server 命令 | [docs/cli/life_codex.md](docs/cli/life_codex.md) |
| Plugin | Emby 字幕插件 | `emby_plugins/video_subtitle/` | `LifeTools.Emby.VideoSubtitle.Emby.dll` | 在 Emby 后台调用 `video_subtitle` 生成字幕 | [docs/plugins/emby_video_subtitle.md](docs/plugins/emby_video_subtitle.md) |
| GUI | InterviewTimer | `gui/interview_timer/` | `InterviewTimer.app` | macOS 面试悬浮计时器 | [docs/gui/interview_timer.md](docs/gui/interview_timer.md) |

Expand Down
6 changes: 6 additions & 0 deletions build.sh
Original file line number Diff line number Diff line change
Expand Up @@ -16,3 +16,9 @@ go build -v -o ./output/codex_hook_notify ./cli/codex_hook_notify/...

# HTTP 文件快速分享
go build -v -o ./output/file_share ./cli/file_share/...

# 远程 Codex 控制台中心服务
go build -v -o ./output/life_codex_server ./cli/life_codex_server/...

# 远程 Codex 控制台机器 agent
go build -v -o ./output/life_codex_agent ./cli/life_codex_agent/...
127 changes: 127 additions & 0 deletions cli/life_codex_agent/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
package main

import (
"context"
"flag"
"fmt"
"log"
"os"
"os/signal"
"strings"

lifecodex "life_tools/internal/life_codex"
)

func main() {
if len(os.Args) < 2 {
usage()
os.Exit(2)
}
switch os.Args[1] {
case "enroll":
runEnroll(os.Args[2:])
case "serve":
runServe(os.Args[2:])
default:
usage()
os.Exit(2)
}
}

func runEnroll(args []string) {
flags := flag.NewFlagSet("enroll", flag.ExitOnError)
configPath := flags.String("config", lifecodex.DefaultAgentConfigPath, "agent config path")
serverURL := flags.String("server", "", "life_codex_server URL")
token := flags.String("token", "", "enrollment token generated by server")
name := flags.String("name", "", "machine name")
roots := flags.String("roots", "", "comma-separated allowed roots")
codexPath := flags.String("codex-path", "", "codex executable path")
attachmentDir := flags.String("attachment-dir", "", "attachment directory")
maxActive := flags.Int("max-active-sessions", 0, "max concurrent sessions")
pollSeconds := flags.Int("poll-seconds", 0, "poll interval seconds")
flags.Parse(args)

if *token == "" {
log.Fatal("--token is required")
}
config := loadAgentConfigLoose(*configPath)
if *serverURL != "" {
config.ServerURL = *serverURL
}
if *name != "" {
config.MachineName = *name
}
if *roots != "" {
config.AllowedRoots = splitCSV(*roots)
}
if *codexPath != "" {
config.CodexPath = *codexPath
}
if *attachmentDir != "" {
config.AttachmentDir = *attachmentDir
}
if *maxActive > 0 {
config.MaxActiveSessions = *maxActive
}
if *pollSeconds > 0 {
config.PollSeconds = *pollSeconds
}
if config.MachineName == "" {
host, _ := os.Hostname()
config.MachineName = host
}
if len(config.AllowedRoots) == 0 {
log.Fatal("--roots is required")
}
resp, err := lifecodex.EnrollAgent(context.Background(), *configPath, config, *token)
if err != nil {
log.Fatal(err)
}
fmt.Printf("enrolled machine %s\n", resp.MachineID)
}

func runServe(args []string) {
flags := flag.NewFlagSet("serve", flag.ExitOnError)
configPath := flags.String("config", lifecodex.DefaultAgentConfigPath, "agent config path")
flags.Parse(args)
config, err := lifecodex.LoadAgentConfig(*configPath)
if err != nil {
log.Fatal(err)
}
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt)
defer stop()
if err := lifecodex.RunAgent(ctx, config); err != nil && ctx.Err() == nil {
log.Fatal(err)
}
}

func loadAgentConfigLoose(path string) lifecodex.AgentConfig {
config, err := lifecodex.LoadAgentConfig(path)
if err == nil {
return config
}
if path != "" && os.IsNotExist(err) {
return lifecodex.DefaultAgentConfig()
}
log.Fatal(err)
return lifecodex.AgentConfig{}
}

func splitCSV(value string) []string {
parts := strings.Split(value, ",")
out := make([]string, 0, len(parts))
for _, part := range parts {
part = strings.TrimSpace(part)
if part != "" {
out = append(out, part)
}
}
return out
}

func usage() {
fmt.Fprintf(os.Stderr, `Usage:
life_codex_agent enroll --server URL --token TOKEN --roots ROOT[,ROOT] [options]
life_codex_agent serve --config PATH
`)
}
110 changes: 110 additions & 0 deletions cli/life_codex_server/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
package main

import (
"context"
"errors"
"flag"
"fmt"
"log"
"net/http"
"os"
"os/signal"
"time"

lifecodex "life_tools/internal/life_codex"
)

func main() {
if len(os.Args) > 1 && os.Args[1] == "audit-clean" {
runAuditClean(os.Args[2:])
return
}
runServe(os.Args[1:])
}

func runServe(args []string) {
flags := flag.NewFlagSet("life_codex_server", flag.ExitOnError)
configPath := flags.String("config", lifecodex.DefaultServerConfigPath, "server config path")
addr := flags.String("addr", "", "listen address override")
dataDir := flags.String("data-dir", "", "data directory override")
auditDir := flags.String("audit-dir", "", "audit directory override")
webRoot := flags.String("web-root", "", "web root override")
adminToken := flags.String("admin-token", "", "admin token override")
flags.Parse(args)

config, err := loadServerConfigForCLI(*configPath)
if err != nil {
log.Fatal(err)
}
if *addr != "" {
config.Addr = *addr
}
if *dataDir != "" {
config.DataDir = *dataDir
}
if *auditDir != "" {
config.AuditDir = *auditDir
}
if *webRoot != "" {
config.WebRoot = *webRoot
}
if *adminToken != "" {
config.AdminToken = *adminToken
}
if config.AdminToken == "" {
log.Fatal("admin_token is required")
}
store, err := lifecodex.NewStateStore(config)
if err != nil {
log.Fatal(err)
}
server := lifecodex.NewHTTPServer(config, store)
httpServer := &http.Server{Addr: config.Addr, Handler: server.Handler()}
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt)
defer stop()
go func() {
<-ctx.Done()
shutdownCtx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
_ = httpServer.Shutdown(shutdownCtx)
}()
log.Printf("life_codex_server listening on http://%s", config.Addr)
if err := httpServer.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) {
log.Fatal(err)
}
}

func runAuditClean(args []string) {
flags := flag.NewFlagSet("audit-clean", flag.ExitOnError)
configPath := flags.String("config", lifecodex.DefaultServerConfigPath, "server config path")
beforeValue := flags.String("before", "", "delete audit files before YYYY-MM-DD")
confirm := flags.Bool("confirm", false, "confirm deletion")
flags.Parse(args)
if !*confirm {
log.Fatal("audit-clean requires --confirm")
}
before, err := time.Parse("2006-01-02", *beforeValue)
if err != nil {
log.Fatal("--before must be YYYY-MM-DD")
}
config, err := loadServerConfigForCLI(*configPath)
if err != nil {
log.Fatal(err)
}
removed, err := lifecodex.ClearAuditBefore(config.AuditDir, before)
if err != nil {
log.Fatal(err)
}
fmt.Printf("removed %d audit file(s)\n", removed)
}

func loadServerConfigForCLI(path string) (lifecodex.ServerConfig, error) {
config, err := lifecodex.LoadServerConfig(path)
if err == nil {
return config, nil
}
if path != "" && os.IsNotExist(err) {
return lifecodex.DefaultServerConfig(), nil
}
return config, err
}
Loading
Loading