File size: 6,890 Bytes
8ede856
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
import { defineStore } from 'pinia';
import axios from 'axios';

export const useCommonStore = defineStore({
  id: 'common',
  state: () => ({
    // @ts-ignore
    eventSource: null,
    log_cache: [],
    sse_connected: false,

    log_cache_max_len: 1000,
    startTime: -1,

    pluginMarketData: [],
  }),
  actions: {
    async createEventSource() {
      if (this.eventSource) {
        return
      }
      const controller = new AbortController();
      const { signal } = controller;
      
      // 注意:这里如果之前改过 Polyfill 的话,可能需要保持原样
      // 如果是用 fetch 的话,这里是支持 Authorization Header 的
      const headers = {
        'Content-Type': 'multipart/form-data',
        'Authorization': 'Bearer ' + localStorage.getItem('token')
      };
      
      fetch('/api/live-log', {
        method: 'GET',
        headers,
        signal,
        cache: 'no-cache',
      }).then(response => {
        if (!response.ok) {
          throw new Error(`SSE connection failed: ${response.status}`);
        }
        console.log('SSE stream opened');
        this.sse_connected = true;

        const reader = response.body.getReader();
        const decoder = new TextDecoder();
        let bufferedText = '';

        const processStream = ({ done, value }) => {
          if (done) {
            console.log('SSE stream closed');
            setTimeout(() => {
              this.eventSource = null;
              this.createEventSource();
            }, 2000);
            return;
          }

          // Accumulate partial chunks; SSE data may split JSON across reads.
          const text = decoder.decode(value, { stream: true });
          bufferedText += text;

          // Split completed events; keep the trailing partial in buffer.
          const segments = bufferedText.split('\n\n');
          bufferedText = segments.pop() || '';

          segments.forEach(segment => {
            const line = segment.trim();
            if (!line.startsWith('data: ')) {
              return;
            }

            const logLine = line.replace('data: ', '').trim();
            if (!logLine) {
              return;
            }

            try {
              const logObject = JSON.parse(logLine);
              
              // 修复:兼容 HTTP 环境的 UUID 生成 
              if (!logObject.uuid) {
                 if (typeof crypto !== 'undefined' && typeof crypto.randomUUID === 'function') {
                    logObject.uuid = crypto.randomUUID();
                 } else {
                    // 手动生成 UUID v4
                    logObject.uuid = 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
                        var r = Math.random() * 16 | 0, v = c == 'x' ? r : (r & 0x3 | 0x8);
                        return v.toString(16);
                    });
                 }
              }

              this.log_cache.push(logObject);
              // Limit log cache size
              if (this.log_cache.length > this.log_cache_max_len) {
                this.log_cache.splice(0, this.log_cache.length - this.log_cache_max_len);
              }
            } catch (err) {
              console.warn('Failed to parse SSE log line, skipping:', err, logLine);
            }
          });
          
          return reader.read().then(processStream);
        };

        reader.read().then(processStream);
      }).catch(error => {
        console.error('SSE error:', error);
        // Attempt to reconnect after a delay
        this.log_cache.push({
            type: 'log',
            level: 'ERROR',
            time: Date.now() / 1000,
            data: 'SSE Connection failed, retrying in 5 seconds...',
            uuid: 'error-' + Date.now() 
        });
        setTimeout(() => {
          this.eventSource = null;
          this.createEventSource();
        }, 1000);
      });

      // Store controller to allow closing the connection
      this.eventSource = controller;
    },
    closeEventSourcet() {
      if (this.eventSource) {
        this.eventSource.abort();
        this.eventSource = null;
      }
    },
    getLogCache() {
      return this.log_cache
    },
    async fetchStartTime() {
      const res = await axios.get('/api/stat/start-time');
      this.startTime = res.data.data.start_time;
      return this.startTime;
    },
    getStartTime() {
      if (this.startTime !== -1) {
        return this.startTime
      }
      this.fetchStartTime().catch(() => {});
      return this.startTime
    },
    async getPluginCollections(force = false, customSource = null) {
      // 获取插件市场数据
      if (!force && this.pluginMarketData.length > 0 && !customSource) {
        return Promise.resolve(this.pluginMarketData);
      }

      // 构建URL
      let url = force ? '/api/plugin/market_list?force_refresh=true' : '/api/plugin/market_list';
      if (customSource) {
        url += (url.includes('?') ? '&' : '?') + `custom_registry=${encodeURIComponent(customSource)}`;
      }

      return axios.get(url)
        .then((res) => {
          let data = []
          if (res.data.data && typeof res.data.data === 'object') {
            for (let key in res.data.data) {
              const pluginData = res.data.data[key];
              
              data.push({
                "name": pluginData.name || key, // 优先使用插件数据中的name字段,否则使用键名
                "desc": pluginData.desc,
                "author": pluginData.author,
                "repo": pluginData.repo,
                "installed": false,
                "version": pluginData?.version ? pluginData.version : "未知",
                "social_link": pluginData?.social_link,
                "tags": pluginData?.tags ? pluginData.tags : [],
                "logo": pluginData?.logo ? pluginData.logo : "",
                "pinned": pluginData?.pinned ? pluginData.pinned : false,
                "stars": pluginData?.stars ? pluginData.stars : 0,
                "updated_at": pluginData?.updated_at ? pluginData.updated_at : "",
                "display_name": pluginData?.display_name ? pluginData.display_name : "",
                "astrbot_version": pluginData?.astrbot_version ? pluginData.astrbot_version : "",
                "support_platforms": Array.isArray(pluginData?.support_platforms)
                  ? pluginData.support_platforms
                  : Array.isArray(pluginData?.support_platform)
                    ? pluginData.support_platform
                    : Array.isArray(pluginData?.platform)
                      ? pluginData.platform
                      : [],
              })
            }
          }
          
          this.pluginMarketData = data;
          return data;
        })
        .catch((err) => {
          return Promise.reject(err);
        });
    },
  }
});