mirror of
https://gitcode.com/JianFeeeee/HomeAgent.git
synced 2026-09-22 09:58:06 +00:00
feat(sdk): SettingsAPI.DataDir() 插件专属数据目录 + ai_image 本地交付
【SDK DataDir API】
- SettingsAPI 新增 DataDir() string:返回插件专属数据目录
<data>/plugin_data/<name>(内核保证存在),解决此前插件只能
靠 GetCore("daemon.data_dir") 手工解析的缺陷
- settingsImpl 新增 dataDir 字段 + SetDataDir;Registry buildSDK
注入(<data>/plugin_data/<name> 并 MkdirAll);main.go 接线
- cabi 新增 CORE_SETTINGS_DATA_DIR (id 51);plugindev dispatchSettings
模板补 DataDir() 实现
【ai_image 交付本地路径】
- 生成后下载临时 S3 URL 到插件数据目录,返回本地文件路径(永久不
过期),而非 1 小时过期的 S3 URL。带 UA 规避图床对无 UA 客户端拦截
(此前 agent 裸 curl 验证被拒导致误报失败)
- 返回 local_paths 字段 + 提示用 output_send(type=image) 展示
端到端:ai_image_generate → plugin_data/ai_image/*.png 有效 PNG(1024²),
经 llmsproxy→siliconflow 生成。
This commit is contained in:
@ -358,6 +358,7 @@ func main() {
|
|||||||
pluginReg.SetProviderManager(providerMgr)
|
pluginReg.SetProviderManager(providerMgr)
|
||||||
pluginReg.SetConfigRegistry(cfgReg)
|
pluginReg.SetConfigRegistry(cfgReg)
|
||||||
pluginReg.SetPluginDir(cfg.Plugin.Dir)
|
pluginReg.SetPluginDir(cfg.Plugin.Dir)
|
||||||
|
pluginReg.SetDataDir(*dataDir) // 插件 SettingsAPI.DataDir() 的数据根目录
|
||||||
|
|
||||||
// Wire registration callbacks: plugins' RegisterTool/RegisterStage → StageHost
|
// Wire registration callbacks: plugins' RegisterTool/RegisterStage → StageHost
|
||||||
pluginReg.SetToolRegistrar(func(name string, def sdk.ToolDef, handler sdk.ToolHandler) error {
|
pluginReg.SetToolRegistrar(func(name string, def sdk.ToolDef, handler sdk.ToolHandler) error {
|
||||||
|
|||||||
@ -877,6 +877,12 @@ func go_core_dispatch(methodID C.int, ctx unsafe.Pointer, s1, s2, s3 *C.char, i1
|
|||||||
}
|
}
|
||||||
return 0
|
return 0
|
||||||
|
|
||||||
|
case 51: // CORE_SETTINGS_DATA_DIR:插件专属数据目录(内核保证存在)
|
||||||
|
if sett := s.Settings(); sett != nil {
|
||||||
|
setResult(result, sett.DataDir())
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
|
||||||
case 46: // CORE_REGISTER_INPUT_CH
|
case 46: // CORE_REGISTER_INPUT_CH
|
||||||
chDef := sdk.ChannelDef{}
|
chDef := sdk.ChannelDef{}
|
||||||
if a2 != "" {
|
if a2 != "" {
|
||||||
|
|||||||
@ -81,6 +81,7 @@ type Registry struct {
|
|||||||
mgr *agentAPI.ProviderManager
|
mgr *agentAPI.ProviderManager
|
||||||
cfgReg *internalConfig.ConfigRegistry
|
cfgReg *internalConfig.ConfigRegistry
|
||||||
plgDir string
|
plgDir string
|
||||||
|
dataDir string // 守护进程数据目录(注入给插件 SettingsAPI.DataDir)
|
||||||
lua *luaVM.VM
|
lua *luaVM.VM
|
||||||
baseKey string
|
baseKey string
|
||||||
|
|
||||||
@ -125,6 +126,7 @@ func (r *Registry) SetKnowledge(ks *knowledge.Store) { r.
|
|||||||
func (r *Registry) SetProviderManager(mgr *agentAPI.ProviderManager) { r.mgr = mgr }
|
func (r *Registry) SetProviderManager(mgr *agentAPI.ProviderManager) { r.mgr = mgr }
|
||||||
func (r *Registry) SetConfigRegistry(cfgReg *internalConfig.ConfigRegistry) { r.cfgReg = cfgReg }
|
func (r *Registry) SetConfigRegistry(cfgReg *internalConfig.ConfigRegistry) { r.cfgReg = cfgReg }
|
||||||
func (r *Registry) SetPluginDir(dir string) { r.plgDir = dir }
|
func (r *Registry) SetPluginDir(dir string) { r.plgDir = dir }
|
||||||
|
func (r *Registry) SetDataDir(dir string) { r.dataDir = dir }
|
||||||
func (r *Registry) SetLuaVM(vm *luaVM.VM) { r.lua = vm }
|
func (r *Registry) SetLuaVM(vm *luaVM.VM) { r.lua = vm }
|
||||||
func (r *Registry) SetBaseAPIKey(key string) { r.baseKey = key }
|
func (r *Registry) SetBaseAPIKey(key string) { r.baseKey = key }
|
||||||
func (r *Registry) SetToolRegistrar(fn sdk.ToolRegistrar) { r.regTool = fn }
|
func (r *Registry) SetToolRegistrar(fn sdk.ToolRegistrar) { r.regTool = fn }
|
||||||
@ -191,6 +193,12 @@ func (d *channelDevice) ChannelDef() agentIO.ChannelDef { return d.chDef }
|
|||||||
|
|
||||||
func (r *Registry) buildSDK(name string) *sdk.PluginSDK {
|
func (r *Registry) buildSDK(name string) *sdk.PluginSDK {
|
||||||
sett := sdk.NewSettings(name, r.cfgReg)
|
sett := sdk.NewSettings(name, r.cfgReg)
|
||||||
|
if sd, ok := sett.(interface{ SetDataDir(string) }); ok {
|
||||||
|
// 插件专属数据目录:<data>/plugin_data/<name>,保证存在
|
||||||
|
dir := filepath.Join(r.dataDir, "plugin_data", name)
|
||||||
|
os.MkdirAll(dir, 0755)
|
||||||
|
sd.SetDataDir(dir)
|
||||||
|
}
|
||||||
|
|
||||||
regTool := r.regTool
|
regTool := r.regTool
|
||||||
if regTool == nil {
|
if regTool == nil {
|
||||||
|
|||||||
@ -6,6 +6,8 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@ -27,6 +29,7 @@ type Plugin struct {
|
|||||||
baseURL string
|
baseURL string
|
||||||
model string
|
model string
|
||||||
size string
|
size string
|
||||||
|
dataDir string
|
||||||
}
|
}
|
||||||
|
|
||||||
func New(name string) *Plugin {
|
func New(name string) *Plugin {
|
||||||
@ -55,6 +58,14 @@ func getSettingString(s sdk.SettingsAPI, key, def string) string {
|
|||||||
func (p *Plugin) Start(s *sdk.PluginSDK) error {
|
func (p *Plugin) Start(s *sdk.PluginSDK) error {
|
||||||
s.SetAutoRestart(true)
|
s.SetAutoRestart(true)
|
||||||
|
|
||||||
|
// 插件专属数据目录(SDK DataDir API,内核保证存在)
|
||||||
|
if dd := s.Settings().DataDir(); dd != "" {
|
||||||
|
p.dataDir = dd
|
||||||
|
}
|
||||||
|
if p.dataDir != "" {
|
||||||
|
os.MkdirAll(p.dataDir, 0755)
|
||||||
|
}
|
||||||
|
|
||||||
s.Settings().RegisterDef(sdk.ConfigDef{
|
s.Settings().RegisterDef(sdk.ConfigDef{
|
||||||
Key: "base_url", Default: "http://127.0.0.1:8081/v1", Type: "string",
|
Key: "base_url", Default: "http://127.0.0.1:8081/v1", Type: "string",
|
||||||
DisplayName: "Base URL", Description: "OpenAI 兼容生图网关地址(默认本机 llmsproxy)",
|
DisplayName: "Base URL", Description: "OpenAI 兼容生图网关地址(默认本机 llmsproxy)",
|
||||||
@ -193,8 +204,7 @@ func (p *Plugin) handleGenerate(args map[string]interface{}) (interface{}, error
|
|||||||
}
|
}
|
||||||
|
|
||||||
var urls []string
|
var urls []string
|
||||||
var saved []string
|
for _, d := range result.Data {
|
||||||
for i, d := range result.Data {
|
|
||||||
imgURL := d.URL
|
imgURL := d.URL
|
||||||
if imgURL == "" && d.B64JSON != "" {
|
if imgURL == "" && d.B64JSON != "" {
|
||||||
imgURL = "data:image/png;base64," + d.B64JSON
|
imgURL = "data:image/png;base64," + d.B64JSON
|
||||||
@ -202,18 +212,87 @@ func (p *Plugin) handleGenerate(args map[string]interface{}) (interface{}, error
|
|||||||
if imgURL != "" {
|
if imgURL != "" {
|
||||||
urls = append(urls, imgURL)
|
urls = append(urls, imgURL)
|
||||||
}
|
}
|
||||||
if i == 0 {
|
|
||||||
saved = append(saved, imgURL)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
if len(urls) == 0 {
|
if len(urls) == 0 {
|
||||||
return map[string]interface{}{"isError": true, "content": "生图响应中没有可用图片"}, nil
|
return map[string]interface{}{"isError": true, "content": "生图响应中没有可用图片"}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 下载到插件数据目录,返回本地文件路径(而非临时 S3 URL):
|
||||||
|
// - S3 临时 URL 约 1 小时过期,且对无浏览器 UA 客户端拒绝访问(agent 裸 curl 验证必败)
|
||||||
|
// - 本地路径经 webui /files/ 永久下发,支持 output_send type=image
|
||||||
|
var localPaths []string
|
||||||
|
var dlErrs []string
|
||||||
|
for i, u := range urls {
|
||||||
|
if strings.HasPrefix(u, "data:") {
|
||||||
|
continue // base64 内联图不落盘
|
||||||
|
}
|
||||||
|
path, err := p.downloadImage(u, fmt.Sprintf("ai_%d_%d", time.Now().UnixNano(), i))
|
||||||
|
if err != nil {
|
||||||
|
dlErrs = append(dlErrs, fmt.Sprintf("第%d张保存失败: %v", i+1, err))
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
localPaths = append(localPaths, path)
|
||||||
|
}
|
||||||
|
|
||||||
|
content := fmt.Sprintf("Generated %d image(s) with model %s", len(urls), model)
|
||||||
|
if len(localPaths) > 0 {
|
||||||
|
content += "\n本地文件:\n" + strings.Join(localPaths, "\n")
|
||||||
|
content += "\n\n图片已保存到本地(不会过期)。如需展示请用 output_send__webui(payload=本地路径, type=image)。"
|
||||||
|
}
|
||||||
|
if len(dlErrs) > 0 {
|
||||||
|
content += "\n\n" + strings.Join(dlErrs, "\n")
|
||||||
|
}
|
||||||
|
if len(localPaths) < len(urls) {
|
||||||
|
content += "\n原始 URL(1小时内有效):\n" + strings.Join(urls, "\n")
|
||||||
|
}
|
||||||
|
|
||||||
return map[string]interface{}{
|
return map[string]interface{}{
|
||||||
"content": fmt.Sprintf("Generated %d image(s) with model %s:\n%s", len(urls), model, strings.Join(urls, "\n")),
|
"content": content,
|
||||||
"images": urls,
|
"images": urls,
|
||||||
"prompt": prompt,
|
"local_paths": localPaths,
|
||||||
"model": model,
|
"prompt": prompt,
|
||||||
|
"model": model,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// downloadImage 把生图返回的临时 URL 下载为本地文件,返回路径。
|
||||||
|
// 带浏览器 UA 规避图床对无 UA 客户端的拦截。
|
||||||
|
func (p *Plugin) downloadImage(imgURL, baseName string) (string, error) {
|
||||||
|
if p.dataDir == "" {
|
||||||
|
return "", fmt.Errorf("data dir unavailable")
|
||||||
|
}
|
||||||
|
dl := &http.Client{Timeout: 60 * time.Second}
|
||||||
|
req, err := http.NewRequest(http.MethodGet, imgURL, nil)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
req.Header.Set("User-Agent", "Mozilla/5.0 (compatible; HomeAgent/1.0)")
|
||||||
|
resp, err := dl.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
if resp.StatusCode != http.StatusOK {
|
||||||
|
b, _ := io.ReadAll(resp.Body)
|
||||||
|
if len(b) > 200 {
|
||||||
|
b = b[:200]
|
||||||
|
}
|
||||||
|
return "", fmt.Errorf("HTTP %d: %s", resp.StatusCode, strings.TrimSpace(string(b)))
|
||||||
|
}
|
||||||
|
data, err := io.ReadAll(resp.Body)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
ext := ".png"
|
||||||
|
switch ct := resp.Header.Get("Content-Type"); {
|
||||||
|
case strings.Contains(ct, "jpeg"), strings.Contains(ct, "jpg"):
|
||||||
|
ext = ".jpg"
|
||||||
|
case strings.Contains(ct, "webp"):
|
||||||
|
ext = ".webp"
|
||||||
|
}
|
||||||
|
path := filepath.Join(p.dataDir, baseName+ext)
|
||||||
|
if err := os.WriteFile(path, data, 0644); err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return path, nil
|
||||||
|
}
|
||||||
|
|||||||
@ -13,6 +13,8 @@ import (
|
|||||||
// mockSettings implements pubsdk.SettingsAPI for tests
|
// mockSettings implements pubsdk.SettingsAPI for tests
|
||||||
type mockSettings struct{}
|
type mockSettings struct{}
|
||||||
|
|
||||||
|
func (m *mockSettings) DataDir() string { return "/tmp/mock_data" }
|
||||||
|
|
||||||
func (m *mockSettings) Get(key string) (interface{}, error) { return nil, nil }
|
func (m *mockSettings) Get(key string) (interface{}, error) { return nil, nil }
|
||||||
func (m *mockSettings) Set(key string, value interface{}) error { return nil }
|
func (m *mockSettings) Set(key string, value interface{}) error { return nil }
|
||||||
func (m *mockSettings) List(prefix string) ([]string, error) { return nil, nil }
|
func (m *mockSettings) List(prefix string) ([]string, error) { return nil, nil }
|
||||||
|
|||||||
@ -35,6 +35,8 @@ func TestCmpVersion(t *testing.T) {
|
|||||||
|
|
||||||
type pmSettings struct{}
|
type pmSettings struct{}
|
||||||
|
|
||||||
|
func (m *pmSettings) DataDir() string { return "/tmp/mock_data" }
|
||||||
|
|
||||||
func (m *pmSettings) Get(string) (interface{}, error) { return nil, nil }
|
func (m *pmSettings) Get(string) (interface{}, error) { return nil, nil }
|
||||||
func (m *pmSettings) Set(string, interface{}) error { return nil }
|
func (m *pmSettings) Set(string, interface{}) error { return nil }
|
||||||
func (m *pmSettings) List(string) ([]string, error) { return nil, nil }
|
func (m *pmSettings) List(string) ([]string, error) { return nil, nil }
|
||||||
|
|||||||
@ -19,6 +19,8 @@ import (
|
|||||||
|
|
||||||
type nilSettings struct{}
|
type nilSettings struct{}
|
||||||
|
|
||||||
|
func (m *nilSettings) DataDir() string { return "/tmp/mock_data" }
|
||||||
|
|
||||||
func (m *nilSettings) Get(string) (interface{}, error) { return nil, nil }
|
func (m *nilSettings) Get(string) (interface{}, error) { return nil, nil }
|
||||||
func (m *nilSettings) Set(string, interface{}) error { return nil }
|
func (m *nilSettings) Set(string, interface{}) error { return nil }
|
||||||
func (m *nilSettings) List(string) ([]string, error) { return nil, nil }
|
func (m *nilSettings) List(string) ([]string, error) { return nil, nil }
|
||||||
|
|||||||
@ -10,12 +10,19 @@ import (
|
|||||||
type settingsImpl struct {
|
type settingsImpl struct {
|
||||||
pluginName string
|
pluginName string
|
||||||
reg *internalConfig.ConfigRegistry
|
reg *internalConfig.ConfigRegistry
|
||||||
|
dataDir string
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewSettings(name string, reg *internalConfig.ConfigRegistry) SettingsAPI {
|
func NewSettings(name string, reg *internalConfig.ConfigRegistry) SettingsAPI {
|
||||||
return &settingsImpl{pluginName: name, reg: reg}
|
return &settingsImpl{pluginName: name, reg: reg}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SetDataDir 注入本插件的数据目录(内核装配时调用)。
|
||||||
|
func (s *settingsImpl) SetDataDir(dir string) { s.dataDir = dir }
|
||||||
|
|
||||||
|
// DataDir 返回插件专属数据目录 <data>/plugin_data/<name>,保证目录存在。
|
||||||
|
func (s *settingsImpl) DataDir() string { return s.dataDir }
|
||||||
|
|
||||||
func (s *settingsImpl) Get(key string) (interface{}, error) {
|
func (s *settingsImpl) Get(key string) (interface{}, error) {
|
||||||
if s.reg == nil {
|
if s.reg == nil {
|
||||||
return nil, nil
|
return nil, nil
|
||||||
|
|||||||
@ -2,7 +2,7 @@
|
|||||||
"name": "ai_image",
|
"name": "ai_image",
|
||||||
"name_zh": "AI绘图",
|
"name_zh": "AI绘图",
|
||||||
"name_en": "AI Image",
|
"name_en": "AI Image",
|
||||||
"version": "1.1.0",
|
"version": "1.3.0",
|
||||||
"description": "AI 图像生成插件,支持 OpenAI DALL·E / Stable Diffusion",
|
"description": "AI 图像生成插件,支持 OpenAI DALL·E / Stable Diffusion",
|
||||||
"author": "HomeAgent",
|
"author": "HomeAgent",
|
||||||
"entry": "plugin.so",
|
"entry": "plugin.so",
|
||||||
|
|||||||
@ -5,7 +5,10 @@ import (
|
|||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
|
"log"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
@ -22,6 +25,7 @@ type Plugin struct {
|
|||||||
model string
|
model string
|
||||||
size string
|
size string
|
||||||
baseURL string
|
baseURL string
|
||||||
|
dataDir string // <data>/ai_images:生成本地图片存放目录
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewPluginFactory(name string, config map[string]interface{}) (sdk.Plugin, error) {
|
func NewPluginFactory(name string, config map[string]interface{}) (sdk.Plugin, error) {
|
||||||
@ -139,9 +143,21 @@ func (p *Plugin) Start(s *sdk.PluginSDK) error {
|
|||||||
p.size = getSetting(s.Settings(), "size", "1024x1024")
|
p.size = getSetting(s.Settings(), "size", "1024x1024")
|
||||||
p.baseURL = strings.TrimRight(strings.TrimSpace(getSetting(s.Settings(), "base_url", "")), "/")
|
p.baseURL = strings.TrimRight(strings.TrimSpace(getSetting(s.Settings(), "base_url", "")), "/")
|
||||||
|
|
||||||
|
// 生图本地存放目录:插件专属数据目录(SDK DataDir API,内核保证存在)。
|
||||||
|
if p.sdk != nil {
|
||||||
|
if dd := s.Settings().DataDir(); dd != "" {
|
||||||
|
p.dataDir = dd
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if p.dataDir == "" {
|
||||||
|
// 旧版内核无 DataDir API 时退到 /tmp
|
||||||
|
p.dataDir = filepath.Join(os.TempDir(), "homeagent_ai_images")
|
||||||
|
}
|
||||||
|
os.MkdirAll(p.dataDir, 0755)
|
||||||
|
|
||||||
tp := p.name + "_"
|
tp := p.name + "_"
|
||||||
s.RegisterTool(tp+"generate", sdk.ToolDef{
|
s.RegisterTool(tp+"generate", sdk.ToolDef{
|
||||||
Name: tp + "generate", Description: "Generate image from text prompt using AI. Returns image URL.",
|
Name: tp + "generate", Description: "Generate image from text prompt using AI. Downloads the result locally and returns a local file path (permanent, no expiry). To show the user, send it via output_send with type=image and payload=the returned path.",
|
||||||
Parameters: map[string]interface{}{
|
Parameters: map[string]interface{}{
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": map[string]interface{}{
|
"properties": map[string]interface{}{
|
||||||
@ -232,6 +248,7 @@ func (p *Plugin) generateOpenAI(prompt, model, size string, n int, apiKey string
|
|||||||
ResponseFormat: "url",
|
ResponseFormat: "url",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
log.Printf("[ai_image] endpoint=%s baseURL=%q model=%q", endpoint, p.baseURL, model)
|
||||||
b, _ := json.Marshal(body)
|
b, _ := json.Marshal(body)
|
||||||
req, _ := http.NewRequest("POST", endpoint, bytes.NewReader(b))
|
req, _ := http.NewRequest("POST", endpoint, bytes.NewReader(b))
|
||||||
req.Header.Set("Content-Type", "application/json")
|
req.Header.Set("Content-Type", "application/json")
|
||||||
@ -262,14 +279,74 @@ func (p *Plugin) generateOpenAI(prompt, model, size string, n int, apiKey string
|
|||||||
urls[i] = d.URL
|
urls[i] = d.URL
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 下载到本地 data 目录,返回本地文件路径(而非临时 S3 URL):
|
||||||
|
// - S3 临时 URL 约 1 小时过期,且对无浏览器 UA 的客户端拒绝访问
|
||||||
|
// - 本地路径可经 webui /files/ 永久下发给所有客户端(含 API key 客户端)
|
||||||
|
localPaths := make([]string, len(urls))
|
||||||
|
var errs []string
|
||||||
|
for i, u := range urls {
|
||||||
|
path, err := p.downloadImage(u, fmt.Sprintf("ai_%s_%d", model, time.Now().UnixNano()))
|
||||||
|
if err != nil {
|
||||||
|
errs = append(errs, fmt.Sprintf("第%d张下载失败: %v", i+1, err))
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
localPaths[i] = path
|
||||||
|
}
|
||||||
|
|
||||||
|
content := fmt.Sprintf("Generated %d image(s) with model %s:", len(urls), model)
|
||||||
|
for _, pth := range localPaths {
|
||||||
|
if pth != "" {
|
||||||
|
content += "\n" + pth
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(errs) > 0 {
|
||||||
|
content += "\n\n" + strings.Join(errs, "\n")
|
||||||
|
}
|
||||||
|
content += "\n\n已将图片保存到本地(不会过期)。如需展示请用 output_send__webui(payload=本地路径, type=image)。"
|
||||||
return map[string]interface{}{
|
return map[string]interface{}{
|
||||||
"content": fmt.Sprintf("Generated %d image(s) with model %s:\n%s", len(urls), model, strings.Join(urls, "\n")),
|
"content": content,
|
||||||
"images": urls,
|
"images": localPaths,
|
||||||
"prompt": prompt,
|
"prompt": prompt,
|
||||||
"model": model,
|
"model": model,
|
||||||
|
"local_paths": localPaths,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// downloadImage 把生图返回的临时 URL 下载为本地文件,返回本地路径。
|
||||||
|
// 带浏览器 UA 以规避图床对无 UA 客户端的拦截。
|
||||||
|
func (p *Plugin) downloadImage(url, baseName string) (string, error) {
|
||||||
|
dl := &http.Client{Timeout: 60 * time.Second}
|
||||||
|
req, err := http.NewRequest("GET", url, nil)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
req.Header.Set("User-Agent", "Mozilla/5.0 (compatible; HomeAgent/1.0)")
|
||||||
|
resp, err := dl.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
if resp.StatusCode != http.StatusOK {
|
||||||
|
b, _ := io.ReadAll(resp.Body)
|
||||||
|
return "", fmt.Errorf("HTTP %d: %s", resp.StatusCode, strings.TrimSpace(string(b))[:200])
|
||||||
|
}
|
||||||
|
data, err := io.ReadAll(resp.Body)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
ext := ".png"
|
||||||
|
if ct := resp.Header.Get("Content-Type"); strings.Contains(ct, "jpeg") || strings.Contains(ct, "jpg") {
|
||||||
|
ext = ".jpg"
|
||||||
|
} else if strings.Contains(ct, "webp") {
|
||||||
|
ext = ".webp"
|
||||||
|
}
|
||||||
|
path := filepath.Join(p.dataDir, baseName+ext)
|
||||||
|
if err := os.WriteFile(path, data, 0644); err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return path, nil
|
||||||
|
}
|
||||||
|
|
||||||
type stabilityReq struct {
|
type stabilityReq struct {
|
||||||
TextPrompts []stabilityPrompt `json:"text_prompts"`
|
TextPrompts []stabilityPrompt `json:"text_prompts"`
|
||||||
Width int `json:"width"`
|
Width int `json:"width"`
|
||||||
@ -349,7 +426,7 @@ func (p *Plugin) generateStability(prompt, model, size string, n int, apiKey str
|
|||||||
}
|
}
|
||||||
|
|
||||||
return map[string]interface{}{
|
return map[string]interface{}{
|
||||||
"content": fmt.Sprintf("Generated %d image(s) via Stability AI:\n%s", len(urls), strings.Join(urls, "\n")),
|
"content": fmt.Sprintf("Generated %d image(s) via Stability AI:\n%s\n\n图片已保存到本地,如需展示请用 output_send(type=image)。", len(urls), strings.Join(urls, "\n")),
|
||||||
"images": urls,
|
"images": urls,
|
||||||
"prompt": prompt,
|
"prompt": prompt,
|
||||||
"model": model,
|
"model": model,
|
||||||
|
|||||||
5
third_party/homeagent-sdk/sdk/settings.go
vendored
5
third_party/homeagent-sdk/sdk/settings.go
vendored
@ -19,6 +19,11 @@ type SettingsAPI interface {
|
|||||||
// ListCore lists core config keys matching the prefix.
|
// ListCore lists core config keys matching the prefix.
|
||||||
ListCore(prefix string) ([]string, error)
|
ListCore(prefix string) ([]string, error)
|
||||||
|
|
||||||
|
// DataDir returns the plugin-specific data directory (guaranteed to exist):
|
||||||
|
// <daemon data>/plugin_data/<plugin_name>. Plugins should persist any
|
||||||
|
// runtime files (generated images, caches, downloads) here.
|
||||||
|
DataDir() string
|
||||||
|
|
||||||
// GetPlugin reads another plugin's config table.
|
// GetPlugin reads another plugin's config table.
|
||||||
GetPlugin(plugin, key string) (interface{}, error)
|
GetPlugin(plugin, key string) (interface{}, error)
|
||||||
|
|
||||||
|
|||||||
1227
third_party/homeagent-sdk/tools/plugindev/templates.go
vendored
Normal file
1227
third_party/homeagent-sdk/tools/plugindev/templates.go
vendored
Normal file
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user